Internal Links:
- [[【Technical Perspective on xlog】Smoother User Experience 3 Implementing an Obsidian Plugin]]
Related:
- Official documentation link https://docs.obsidian.md/Home
- I created an Obsidian Plugin Vue Template
There is a lot of official content:
- Plugin development, the core of this session
- Theme development. Ignored this time
- Refer listing APIs
- Others
You can scroll down to see quick code snippets
Plugin Development#
I want to explain the structure and basic usage by introducing a simple plugin written in pure JS. This plugin is very practical and can help us better develop and debug. For convenience, I recommend first installing a plugin called "hot-reload," which you can find at https://github.com/pjeby/hot-reload.
For users using Vue, I also recommend a template that combines Vue3, Vite4, and Obsidian plugins, making it easier for you to start development. You can find this template at https://github.com/Otto-J/Obsidian-Vue-Starter.
Before explaining the specific usage of the plugin, I want to introduce the plugin lifecycle. The plugin lifecycle includes two phases: onload and onunload.
Mobile plugins are ignored.
Pages developed with Vue are mounted onto ItemView. This means we can use various Vue features in ItemView to develop custom pages.
- Start
- Introduce the structure and basic usage through a simple pure JS plugin
- Be sure to install https://github.com/pjeby/hot-reload early for development and debugging
- Vue users can use this template, vue3+vite4+obsidian plugin
- Lifecycle onload - onunload
- Mobile plugins provide how to enable simulation, just to know
- Pages developed with Vue are mounted onto ItemView
- UI
- Introduction to UI structure, aligning the names of different areas
- Show an image that introduces different positions; sidebar is called ribbon actions, which is a bit strange
- This is the image
- 【Command Palette】
- Similar to vsCode, use cmd + p to bring up the command palette; plugin development registers using this.addCommand method in onload, which is not complicated, for example, handling the content of the currently active editor
- Commands can have pre-checks using the checkCallback method to verify if they should run, adding them all saves trouble
- You can access the editor with the editorCallback method, which has parameters for editor and view, and there is also an editorCheckCallback option
- You can register hotkeys to add another trigger method. Details ignored
- 【Right-click Menu】
- Import
obsidian.Menu
, instantiate menu.addItem can be added, this is placed in the sidebar - It can also be placed in the file list file-menu and editor-menu at the top right of the editor
- Import
- 【HTML Container】 can place HTML containers in the plugin settings, in PluginSettingTab
- You can use traditional JS createEl, similar to h rendering functions.
- The styles.css in the plugin root directory, use Vue to simplify
- 【Icons】 can be found here, https://lucide.dev/ supports Flutter quite well
- Custom icons need to prepare the original SVG content, no need to worry about Vue icons
- 【Modals】 pop-ups for user input
- Import interface from Obsidian.Modal, need to implement onOpen and onClose methods
- There are corresponding callbacks when clicking submit
- There is also a SuggestModal for assisting input, and FuzzySuggestModal can be ignored for now
- 【Sidebar】 the official term is ribbon actions
- Usage is this.addRibbonIcon, which will definitely be mentioned in the demo
- 【Settings】 this is the public settings, where the plugin fills in configurations
- Plugin settings are persistent, so the process is to first load this.loadData to merge and get the configuration result
- The official example code is the same as the documentation
- 【Status Bar】 the status bar at the bottom right
- this.addStatusBarItem to add
- Multiple can be added
- 【Views】 resource management, editor, preview are all views
- Get view ID getViewType
- 【Workspace】 not understood, tree structure
- Can open multiple documents side by side, split way
- Not understood, the documentation is poorly written
- The keyword is Leaf something something
- Just look at the quick operation code snippets below
- Introduction to UI structure, aligning the names of different areas
- Editor
- 【Editor】 Class to get and edit the MD in editing mode
- See the code below
- 【Markdown Post-processing】 the MD in preview mode is HTML, can adjust HTML
- Example 1, replace with emoji icons
- Example 2, parse and add special syntax content, oh, it's data transformation, like converting CSV to table
- 【Editor Extensions】 can be ignored for now, introducing CodeMirror6 content
- The underlying dependency is CodeMirror6, which is used for writing extensions, not looking at this now
- 【Decorators】 supplement styles for the above editor extension content, not related
- Not looking at this now
- 【State Fields】 editor extension, not understood
- 【State Management】 state management for editor extensions
- If you want to undo, you need to retain the state
- 【View Plugins】 editor extension plugins, can access viewport, ignored
- 【Viewport】 high-performance editors cannot do without virtual scrolling to avoid lag; the viewport will update and recalculate when scrolling the document
- 【Editor Communication】 ignored
- 【Editor】 Class to get and edit the MD in editing mode
- Releasing
- CI operations - the documentation provides a Github action to assist in building plugins
- Before submitting to the official market, provide Readme.md license, mainfest.json, the official example is available
- Just follow the documentation, the official maintains a JSON, just submit a PR
- Note that Node and Electron are only allowed for desktop use
- If it's a beta version, consider the brat plugin
- Event
- We might want a timer, like setTimeInterval to try updating something; there is a dedicated this.registerInterval method
this.registerInterval( window.setInterval(() => this.updateStatusBar(), 1000) );
- Time formatting, built-in obsidian.moment
- We might want a timer, like setTimeInterval to try updating something; there is a dedicated this.registerInterval method
- Vault
- A collection of notes, encapsulating these FS operations
- Document read-only without operations use cachedRead()
- Document read and modify can use read()
- Modify file using vault.modify(), this is an overwrite operation
- vault.process callback address has data indicating current content, only supports synchronous operations
- For asynchronous modifications consider vault.cachedRead() - asynchronous operation - vault.process() update
- Delete file trash() is the recycle bin, permanently delete is delete()
app.vault.getAbstractFileByPath("folderOrFile")
the result of the absolute path may be a file or folder, determined byinstanceof TFile
/TFolder
Common Snippets#
There are two methods to read the content of a file: read() and cachedRead()
- If you only want to display content to the user, use to avoid multiple reads from disk.
cachedRead()
- If you want to read content, modify it, and write it back to disk, then use to avoid possibly overwriting the file with an outdated copy.
read()
// Get the content of the active text
app.workspace.getActiveFileView().data
// Also can
app.workspace.activeEditor
// User activated cursor position
app.workspace.getActiveFileView().editor.getCursor()
// {line:2,ch:2}
// Append text at a specified position, like the current date or template
app.workspace.getActiveFileView().editor.replaceRange('222',{line: 2, ch: 2})
// The currently active editor
const editor = app.workspace.getActiveFileView().editor
// User selected a portion of content
app.workspace.getActiveFileView().editor.getSelection()
// Replace content, for example, after upload becomes CDN address sha
editor.replaceSelection('xxx')
// Get all MD documents
app.vault.getMarkdownFiles()
// Get all files, then filter
app.vault.getFile()
// Get text content
var file = app.vault.getMarkdownFiles()[0]
const content = await app.vault.cachedRead(file)
// Modify content, this is a synchronous operation
app.vault.process(file, (data) => { return data.replace(":)", "🙂"); })
this.app.workspace.on('editor-paste', (evt, editor, view) => {
if (evt.clipboardData) {}
}))
// Can get the current file file
this.app.workspace.on("file-menu", (menu, file) => {
if (file instanceof TFile) {}
});
// Given a string path, check if the file exists, safer
const isExist = await this.app.vault.adapter.exists('xx.md')
// Given a string, find the best matching file
app.metadataCache.getFirstLinkpathDest('folder/download.png','')
// Get TFile to read content
app.metadataCache.getFileCache(file)
// where embeds indicate associated images, but do not include external link standard syntax, need to be used together
// frontmatter is among them
// frontmatterPosition is the start and end position of fm, can be deleted and replaced
// links indicate external links of the current article
// Read binary content of an image, later display and upload the blob
const conArrayBuffer = await plugin.app.vault.readBinary(TFile);
// Convert to binary, upload via post
const blob = new Blob([conArrayBuffer], {
type: "image/" + obInnerFile.extension,
});
Handling front-matter is quite common and troublesome, providing a common solution
export const useObsidianFrontmatter = (file: TFile, app: App) => {
// Use more semantic function names
const doesFileExist = () => !!app.metadataCache.getFileCache(file);
const currentFrontMatter = () =>
app.metadataCache.getFileCache(file)?.frontmatter ?? {};
const addOrUpdateFrontMatter = async (obj: Record<string, string>) => {
const fileCache = app.metadataCache.getFileCache(file);
// If the file does not exist, return directly
if (!fileCache) {
new Notice("File does not exist");
return;
}
const currentFrontMatter = fileCache?.frontmatter ?? {};
const newFrontMatter = `---\n${stringifyYaml({
...currentFrontMatter,
...obj,
})}\n---\n`;
// const { frontmatterPosition } = fileCache;
// readcontent or con = vault.cachedRead(file)
const fileContents = await app.vault.read(file);
const frontmatterPosition = fileCache.frontmatterPosition ?? {
start: {
line: 0,
col: 0,
offset: 0,
},
end: {
line: 0,
col: 0,
offset: 0,
},
};
// The logic here is a bit convoluted, the goal is to rewrite the file content; if there is an API later, it might be solved with one line of code, similar to metadataCache.update
const {
start: { offset: deleteFrom },
end: { offset: deleteTo },
} = frontmatterPosition;
const newFileContents =
fileContents.slice(0, deleteFrom) +
newFrontMatter +
fileContents.slice(deleteTo);
await app.vault.modify(file, newFileContents);
};
return {
doesFileExist,
addOrUpdateFrontMatter,
currentFrontMatter,
};
};
Quick Familiarization with TS API#
Official Documentation is particularly poorly written, and I haven't seen good introductions around.
Quickly list
- abstract-input-suggest provides some hints in the input box, like historical tags
- abstract-text-component seems to be related to text input box abstract classes
- app application-level properties, can access files, vault, hotkeys, workspace, etc.
- base-component can set disabled
- class BlockCache extends CacheItem
- block-sub-path-result
- button-component button component, no need to look, unified packaging
- cached-metadata--interface
- cache-item--interface
- closeable-component--class provides close methods, no need to look, all packaged
- ColorComponent--class, color picker, can get color hex, can be packaged
- Command--interface registers command-related methods
- component--class core entry component
- data-adapter--interface handles files and temperature, recommended Vault's API
- data-write-options--interface
- debouncer--interface
- dropdown-component--class can be packaged
- Editor--class smooths out differences in codemirror versions, should provide many auxiliary functions
- editor-change--interface
- editor-position--interface
- editor-range--interface
- editor-range-or-caret--interface
- editor-scroll-info
- editorxxx skipped
- events--class related to events
- extra-button-component--class packaged
- file-manager--class manages file creation, deletion, renaming from a UI perspective
- file-stats--interface
- file-system-adapter implements DataAdapter--class