├── .eslintignore ├── versions.json ├── .gitignore ├── manifest.json ├── manifest-beta.json ├── tsconfig.json ├── .eslintrc ├── styles.css ├── package.json ├── esbuild.config.mjs ├── src └── main.ts └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | npm node_modules 2 | build -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.0.1": "0.13.8" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | package-lock.json 11 | 12 | # Don't include the compiled main.js file in the repo. 13 | # They should be uploaded to GitHub releases instead. 14 | main.js 15 | 16 | # Exclude sourcemaps 17 | *.map 18 | 19 | # obsidian 20 | data.json 21 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-cm6-attributees", 3 | "name": "CM6 Attributees", 4 | "version": "0.0.3", 5 | "minAppVersion": "0.13.8", 6 | "description": "This is an experimental attributes plugin for CM6. Use with caution as it is very alpha.", 7 | "author": "NothingIsLost", 8 | "authorUrl": "https://github.com/nothingislost", 9 | "isDesktopOnly": true 10 | } 11 | -------------------------------------------------------------------------------- /manifest-beta.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-cm6-attributees", 3 | "name": "CM6 Attributees", 4 | "version": "0.0.5", 5 | "minAppVersion": "0.13.8", 6 | "description": "This is an experimental attributes plugin for CM6. Use with caution as it is very alpha.", 7 | "author": "NothingIsLost", 8 | "authorUrl": "https://github.com/nothingislost", 9 | "isDesktopOnly": true 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "lib": [ 13 | "DOM", 14 | "ES5", 15 | "ES6", 16 | "ES7" 17 | ] 18 | }, 19 | "include": [ 20 | "**/*.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "parserOptions": { 13 | "sourceType": "module" 14 | }, 15 | "rules": { 16 | "no-unused-vars": "off", 17 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 18 | "@typescript-eslint/ban-ts-comment": "off", 19 | "no-prototype-builtins": "off", 20 | "@typescript-eslint/no-empty-function": "off" 21 | } 22 | } -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .cm-list-widget { 2 | cursor: pointer; 3 | margin-left: -0.8em; 4 | } 5 | 6 | .cm-lineNumbers { 7 | padding-right: .5em; 8 | } 9 | 10 | .cm-editor .cm-foldGutter { 11 | opacity: 0; 12 | } 13 | 14 | .cm-editor .cm-gutters { 15 | z-index: 0; 16 | } 17 | 18 | .cm-fold-widget.collapse-indicator { 19 | display: inline-block; 20 | color: var(--text-muted); 21 | cursor: pointer; 22 | } 23 | 24 | .is-collapsed.collapse-icon svg { 25 | transform: rotate(-90deg); 26 | } 27 | 28 | .cm-fold-widget { 29 | position: relative; 30 | } 31 | 32 | .cm-fold-widget svg { 33 | position: absolute; 34 | top: -0.6rem; 35 | left: -12px; 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-cm6-attributes", 3 | "version": "0.0.1", 4 | "description": "This is an experimental attributes plugin for CM6. Use with caution as it is very alpha.", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "node esbuild.config.mjs production" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@codemirror/rangeset": "^0.19.0", 15 | "@codemirror/state": "^0.19.0", 16 | "@codemirror/view": "^0.19.0", 17 | "@codemirror/commands": "^0.19.0", 18 | "@codemirror/fold": "0.19.0", 19 | "@codemirror/history": "^0.19.0", 20 | "@codemirror/language": "^0.19.0", 21 | "@codemirror/matchbrackets": "^0.19.0", 22 | "@codemirror/panel": "^0.19.0", 23 | "@codemirror/search": "^0.19.0", 24 | "@codemirror/stream-parser": "https://github.com/lishid/stream-parser", 25 | "@types/node": "^16.11.6", 26 | "@typescript-eslint/eslint-plugin": "^5.2.0", 27 | "@typescript-eslint/parser": "^5.2.0", 28 | "builtin-modules": "^3.2.0", 29 | "esbuild": "0.13.12", 30 | "obsidian": "^0.13.8", 31 | "tslib": "2.3.1", 32 | "typescript": "4.4.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | 5 | const banner = `/* 6 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 7 | if you want to view the source, please visit the github repository of this plugin 8 | */ 9 | `; 10 | 11 | const prod = process.argv[2] === "production"; 12 | 13 | esbuild 14 | .build({ 15 | banner: { 16 | js: banner, 17 | }, 18 | minify: prod ? true : false, 19 | entryPoints: ["src/main.ts"], 20 | bundle: true, 21 | external: [ 22 | "obsidian", 23 | "electron", 24 | "codemirror", 25 | "@codemirror/autocomplete", 26 | "@codemirror/closebrackets", 27 | "@codemirror/collab", 28 | "@codemirror/commands", 29 | "@codemirror/comment", 30 | "@codemirror/fold", 31 | "@codemirror/gutter", 32 | "@codemirror/highlight", 33 | "@codemirror/history", 34 | "@codemirror/language", 35 | "@codemirror/lint", 36 | "@codemirror/matchbrackets", 37 | "@codemirror/panel", 38 | "@codemirror/rangeset", 39 | "@codemirror/rectangular-selection", 40 | "@codemirror/search", 41 | "@codemirror/state", 42 | "@codemirror/stream-parser", 43 | "@codemirror/text", 44 | "@codemirror/tooltip", 45 | "@codemirror/view", 46 | "@lezer/common", 47 | "@lezer/lr", 48 | ...builtins, 49 | ], 50 | format: "cjs", 51 | watch: !prod, 52 | target: "es2016", 53 | logLevel: "info", 54 | sourcemap: prod ? false : "inline", 55 | treeShaking: true, 56 | outfile: "dist/main.js", 57 | }) 58 | .catch(() => process.exit(1)); 59 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Plugin, setIcon } from "obsidian"; 2 | import { RangeSetBuilder } from "@codemirror/rangeset"; 3 | import { EditorView, Decoration, DecorationSet, ViewUpdate, WidgetType, ViewPlugin } from "@codemirror/view"; 4 | import { EditorState, EditorSelection, TransactionSpec, Transaction } from "@codemirror/state"; 5 | import { syntaxTree, foldable } from "@codemirror/language"; 6 | import { tokenClassNodeProp } from "@codemirror/stream-parser"; 7 | import { foldEffect, unfoldEffect, foldedRanges } from "@codemirror/fold"; 8 | 9 | export default class AttributesPlugin extends Plugin { 10 | async onload() { 11 | const ext = this.buildAttributesViewPlugin(); 12 | this.registerEditorExtension(ext); 13 | } 14 | 15 | buildAttributesViewPlugin() { 16 | // build the DOM element that we'll prepend to list elements 17 | class FoldWidget extends WidgetType { 18 | isFolded: boolean; 19 | isHeader: boolean; 20 | 21 | constructor(isFolded: boolean, isHeader: boolean = false) { 22 | super(); 23 | this.isFolded = isFolded; 24 | this.isHeader = isHeader; 25 | } 26 | 27 | eq(other: FoldWidget) { 28 | return other.isFolded == this.isFolded; 29 | } 30 | 31 | toDOM() { 32 | let el = document.createElement("div"); 33 | el.className = "cm-fold-widget collapse-indicator collapse-icon"; 34 | if (this.isFolded) el.addClass("is-collapsed"); 35 | this.isHeader ? el.addClass("heading-collapse-indicator") : el.addClass("list-collapse-indicator"); 36 | setIcon(el, "right-triangle", 8); 37 | return el; 38 | } 39 | 40 | ignoreEvent() { 41 | return false; 42 | } 43 | } 44 | const viewPlugin = ViewPlugin.fromClass( 45 | class { 46 | decorations: DecorationSet; 47 | lineCache: {}; // TODO: Implement caching 48 | tokenCache: {}; // TODO: Implement caching 49 | 50 | constructor(view: EditorView) { 51 | this.decorations = this.buildDecorations(view); 52 | } 53 | 54 | update(update: ViewUpdate) { 55 | if (update.docChanged || update.viewportChanged) { 56 | this.decorations = this.buildDecorations(update.view); 57 | } else if (update.geometryChanged) { 58 | // this logic is to update the fold widget icons since a fold 59 | // does not trigger docChanged or viewportChanged 60 | // there's probably a better way to do this 61 | for (let tr of update.transactions) { 62 | for (let effect of tr.effects) { 63 | if (effect && effect.value) { 64 | if (effect.is(foldEffect) || effect.is(unfoldEffect)) { 65 | this.decorations = this.buildDecorations(update.view); 66 | } 67 | } 68 | } 69 | } 70 | } 71 | // console.timeEnd("build deco"); 72 | } 73 | 74 | destroy() {} 75 | 76 | buildDecorations(view: EditorView) { 77 | const hashTagRegexp = /#(?:[^\u2000-\u206F\u2E00-\u2E7F'!"#$%&()*+,.:;<=>?@^`{|}~\[\]\\\s])+/g; 78 | let builder = new RangeSetBuilder(); 79 | // use view.visibleRanges rather than view.viewPort since visibleRanges will filter out folded and non visible ranges 80 | for (let { from, to } of view.visibleRanges) { 81 | try { 82 | // syntaxTree gives us access to the tokens generated by the markdown parser 83 | // here we iterate over the visible text and evaluate each token, sequentially. 84 | const tree = syntaxTree(view.state); 85 | tree.iterate({ 86 | from, 87 | to, 88 | enter: (type, from, to) => { 89 | // To access the parsed tokens, we need to use a NodeProp. 90 | // Obsidian exports their inline token NodeProp, tokenClassNodeProp, as part of their 91 | // custom stream-parser package. See the readme for more details. 92 | 93 | const tokenProps = type.prop(tokenClassNodeProp); 94 | 95 | if (tokenProps) { 96 | const props = new Set(tokenProps.split(" ")); 97 | const isTag = props.has("hashtag-end"); 98 | const isList = props.has("formatting-list"); 99 | const isHeader = props.has("formatting-header"); 100 | const isBarelink = props.has("hmd-barelink") && !props.has("formatting"); 101 | 102 | if (isList || isHeader) { 103 | // add a fold icon, inline, next to every foldable list item 104 | // TODO: fix the naive negative margin in styles.css 105 | let range, 106 | line = view.state.doc.lineAt(from); 107 | if ((range = foldable(view.state, line.from, line.to))) { 108 | const isFolded = foldExists(view.state, range.from, range.to); 109 | let deco = Decoration.widget({ 110 | widget: new FoldWidget(isFolded, isHeader), 111 | }); 112 | builder.add(from, from, deco); 113 | } 114 | } 115 | if (isTag) { 116 | // This adds a data-tags attribute to the parent cm-line. 117 | // The attribute value will be a list of all tags found on the line 118 | // TODO: this currently recomputes the entire list of hashtags for a given 119 | // line once for every hashtag found. it works but it could be better. 120 | let line = view.state.doc.lineAt(from); 121 | let deco = Decoration.line({ 122 | attributes: { "data-tags": line.text.match(hashTagRegexp)?.join(" ").replace(/#/g, "") }, 123 | }); 124 | // TODO: Figure out a better way to fix the pos conflict when 125 | // a top level list item has a hashtag at the beginning of the line 126 | // The code below is a hack using internal class properties 127 | if ((builder).lastFrom == line.from) { 128 | // if we don't do this, we get an error stating our rangeset is not sorted 129 | deco.startSide = (builder).last.startSide + 1; 130 | } 131 | builder.add(line.from, line.from, deco); 132 | } 133 | if (isBarelink) { 134 | // add the value of barelinks as an href on the inline element 135 | // this will cause a nested span to be created 136 | let deco = Decoration.mark({ 137 | attributes: { href: view.state.doc.sliceString(from, to) }, 138 | }); 139 | builder.add(from, to, deco); 140 | } 141 | } 142 | }, 143 | }); 144 | } catch (err) { 145 | // cm6 will silently unload extensions when they crash 146 | // this try/catch will provide details when crashes occur 147 | console.error("Custom CM6 view plugin failure", err); 148 | // make to to throw because if you don't, you'll block 149 | // the auto unload and destabilize the editor 150 | throw err; 151 | } 152 | } 153 | return builder.finish(); 154 | } 155 | }, 156 | { 157 | decorations: v => v.decorations, 158 | 159 | eventHandlers: { 160 | // create an event handler for our new fold widget 161 | mousedown: (e, view) => { 162 | // TODO: only act on left click 163 | let target = (e.target as HTMLElement).closest(".cm-fold-widget"); 164 | if (target) { 165 | const foldMarkerPos = view.posAtDOM(target); 166 | const line = view.state.doc.lineAt(foldMarkerPos); 167 | let range = foldable(view.state, line.from, line.to); 168 | if (range) { 169 | let curPos = view.state.selection.main.head; 170 | let effect = foldExists(view.state, range.from, range.to) ? unfoldEffect : foldEffect; 171 | let transaction: TransactionSpec = { effects: [effect.of(range), announceFold(view, range)] }; 172 | if (curPos > range.from && curPos < range.to) { 173 | transaction.selection = EditorSelection.cursor(range.to); 174 | } 175 | view.dispatch(transaction); 176 | return true; 177 | } 178 | } 179 | }, 180 | }, 181 | } 182 | ); 183 | 184 | return viewPlugin; 185 | } 186 | } 187 | 188 | function foldExists(state: EditorState, from: number, to: number) { 189 | // adapted from https://github.com/codemirror/fold/blob/36ca2ec57aa3907fb0d1c13669b51e98e379e583/src/fold.ts#L76 190 | const folded = foldedRanges(state); 191 | let found = false; 192 | folded.between(from, from, (a, b) => { 193 | if (a == from && b == to) found = true; 194 | }); 195 | return found; 196 | } 197 | 198 | function announceFold(view: EditorView, range: { from: number; to: number }, fold = true) { 199 | // copied from https://github.com/codemirror/fold/blob/36ca2ec57aa3907fb0d1c13669b51e98e379e583/src/fold.ts#L110 200 | let lineFrom = view.state.doc.lineAt(range.from).number, 201 | lineTo = view.state.doc.lineAt(range.to).number; 202 | return EditorView.announce.of( 203 | `${view.state.phrase(fold ? "Folded lines" : "Unfolded lines")} ${lineFrom} ${view.state.phrase("to")} ${lineTo}.` 204 | ); 205 | } 206 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Obsidian CM6 Attributes Reference Plugin 2 | 3 | This reference plugin implements a ViewPlugin which will parse the markdown syntaxTree and add various Decorations to enhance the editor. 4 | 5 | The plugin will add: 6 | - An inline fold widget to list items 7 | - An inline href attribute to barelinks 8 | - A .cm-line based data attribute to all lines containing hashtags which will contain the value of all tags found on the line 9 | 10 | The plugin includes examples of the following CodeMirror 6 components: 11 | 12 | - [ViewPlugin](https://codemirror.net/6/docs/ref/#view.ViewPlugin) 13 | - [eventHandlers](https://codemirror.net/6/docs/ref/#view.PluginSpec.eventHandlers) 14 | - Markdown Token Parsing 15 | - [syntaxTree](https://codemirror.net/6/docs/ref/#language.syntaxTree) 16 | - [Code Folding](https://codemirror.net/6/docs/ref/#folding) 17 | - [Decorations](https://codemirror.net/6/docs/ref/#decorations) 18 | - [widget](https://codemirror.net/6/docs/ref/#view.Decoration^widget) 19 | - [mark](https://codemirror.net/6/docs/ref/#view.Decoration^mark) 20 | - [line](https://codemirror.net/6/docs/ref/#view.Decoration^line) 21 | 22 | ## Getting Started with CodeMirror 6 23 | 24 | Before getting started with CodeMirror 6 development, it is HIGHLY recommended that you first read the CodeMirror 6 documentation. CodeMirror 6 is a very complex architecture with many modular components. The CodeMirror 6 author does a great job of explaining the architecture in the [CodeMirror 6 System Guide](https://codemirror.net/6/docs/guide/). This is a long document but it is worth reading from start to finish. 25 | 26 | If you have experience with CodeMirror 5, there's a [CodeMirror 5 -> CodeMirror 6 Migration Guide](https://codemirror.net/6/docs/migration/) that does a great job of explaining how the system has changed between the two versions. It's important that you read this document as these are not subtle changes. The system has changed significantly and if you go into CodeMirror 6 with CodeMirror 5 expectations, you will be swimming upstream. 27 | 28 | Additional resources include [CodeMirror 6 Examples](https://codemirror.net/6/examples/), the [CodeMirror 6 Reference Manual](https://codemirror.net/6/docs/ref/) and the [CodeMirror 6 Discussion Forum](https://discuss.codemirror.net/c/next). 29 | 30 | ## Using CodeMirror 6 with Obsidian 31 | 32 | ### Managing CodeMirror 6 Package Imports 33 | 34 | When building CodeMirror 6 extensions, it is critical that you use the exact CM6 classes that were used to instantiate the editor. For example, if you import a different version of "@codemirror/state" than what is used by Obsidian, your plugin will either fail to register or just not work at all. 35 | 36 | The good news is that, as of version 0.13.8, Obsidian provides a way to ensure that you're always using the exact CM6 classes used by the editor. Obsidian does this by overloading calls to `require` for the "@codemirror" packages so that they can return the exact versions that they use internally. 37 | 38 | What this means, practically, is that you should mark all of your @codemirror dependencies as external in whatever bundler you are using. By marking these as external, you are telling the bundler to not include them in your plugin. For esbuild, it would look like this: 39 | 40 | ```js 41 | external: [ 42 | "obsidian", 43 | "electron", 44 | "codemirror", 45 | "@codemirror/autocomplete", 46 | "@codemirror/closebrackets", 47 | "@codemirror/collab", 48 | "@codemirror/commands", 49 | "@codemirror/comment", 50 | "@codemirror/fold", 51 | "@codemirror/gutter", 52 | "@codemirror/highlight", 53 | "@codemirror/history", 54 | "@codemirror/language", 55 | "@codemirror/lint", 56 | "@codemirror/matchbrackets", 57 | "@codemirror/panel", 58 | "@codemirror/rangeset", 59 | "@codemirror/rectangular-selection", 60 | "@codemirror/search", 61 | "@codemirror/state", 62 | "@codemirror/stream-parser", 63 | "@codemirror/text", 64 | "@codemirror/tooltip", 65 | "@codemirror/view", 66 | "@lezer/common", 67 | "@lezer/lr", 68 | ...builtins, 69 | ] 70 | ``` 71 | 72 | Note that the list above is the comprehensive list of @codemirror packages provided by Obsidian as of version 0.13.8. It is not advised to try and use any @codemirror package that is not in this list. If you attempt to import a @codemirror package not in this list, you run a high risk of introducing package conflicts and subtle bugs. 73 | 74 | With that done, you can now import packages like this `import { StateEffect, StateField, Transaction } from "@codemirror/state";` and be confident that your version of `StateField` will be the exact `StateField` used by Obsidian. 75 | 76 | ### Registering a CodeMirror 6 Extension 77 | 78 | Before continuing further, make sure you understand the [CodeMirror 6 Extension System](https://codemirror.net/6/docs/guide/#extension) and the components involved in [Extending CodeMirror](https://codemirror.net/6/docs/guide/#extending-codemirror). 79 | 80 | Obsidian has provided a helper function to make it easy to register a CM6 extension and manage its lifecycle. The method can be found on the `Plugin` class and is called [registerEditorExtension](https://github.com/obsidianmd/obsidian-api/blob/master/obsidian.d.ts#L2345). 81 | 82 | `registerEditorExtension` takes one argument, a CodeMirror 6 Extension. An Extension can be a single extension or an array of multiple extensions. 83 | 84 | Once registered with `registerEditorExtension`, your extension will be immediately loaded on all active editor instances and all future editor instances. 85 | 86 | `registerEditorExtension` will also handle unloading your extension when your plugin is disabled. 87 | 88 | For cases where you want to get more advanced with your extension management, refer to the documentation on [Dynamic Configuration](https://codemirror.net/6/examples/config/#dynamic-configuration) where they explain how to create and manage `compartments`. Warning, this is an advanced topic. 89 | 90 | ### Additional Obsidian Helper Components 91 | 92 | Obsidian exposes two additional components which can be helpful during plugin development. These components are [StateFields](https://codemirror.net/6/docs/guide/#state-fields) that store a reference to the CM6 `EditorView` and the Obsidian `MarkdownView` in the CM6 `EditorState`. 93 | 94 | The two `StateField` components can be imported from the 'obsidian' package and are named [editorEditorField](https://github.com/obsidianmd/obsidian-api/blob/master/obsidian.d.ts#L808) and [editorViewField](https://github.com/obsidianmd/obsidian-api/blob/master/obsidian.d.ts#L935). 95 | 96 | These fields are useful for getting a reference to the current editor's `EditorView` or `MarkdownView` from a `EditorState` event. 97 | 98 | When you register a `StateField`, your `StateField` will start receiving state events from the editor. Since `EditorState` is completely isolated from the actual `EditorView` in CM6, these state events will provide no indication of what editor or view the event is associated to. 99 | 100 | For example, here is how one might use the `editorEditorField` `StateField` to remove a class from the associated `div.markdown-source-view` element whenever the EditorState has been created or reset: 101 | 102 | ```js 103 | const zoomStateField = StateField.define({ 104 | create(state: EditorState) { 105 | const editorView = state.field(editorEditorField); 106 | editorView.dom.parentElement.removeClass("is-zoomed-in"); 107 | return Decoration.none; 108 | } 109 | ... 110 | }) 111 | ``` 112 | 113 | It is advised to use these `StateField` helpers sparingly. Under normal circumstances, you should keep a clean separation between `EditorState` and `EditorView`. 114 | 115 | ## Sample Plugin Documentation 116 | 117 | ### First time developing plugins? 118 | 119 | Quick starting guide for new plugin devs: 120 | 121 | - Make a copy of this repo as a template with the "Use this template" button (login to GitHub if you don't see it). 122 | - Clone your repo to a local development folder. For convenience, you can place this folder in your `.obsidian/plugins/your-plugin-name` folder. 123 | - Install NodeJS, then run `npm i` in the command line under your repo folder. 124 | - Run `npm run dev` to compile your plugin from `main.ts` to `main.js`. 125 | - Make changes to `main.ts` (or create new `.ts` files). Those changes should be automatically compiled into `main.js`. 126 | - Reload Obsidian to load the new version of your plugin. 127 | - Enable plugin in settings window. 128 | - For updates to the Obsidian API run `npm update` in the command line under your repo folder. 129 | 130 | ### Releasing new releases 131 | 132 | - Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release. 133 | - Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible. 134 | - Create new GitHub release using your new version number as the "Tag version". Use the exact version number, don't include a prefix `v`. See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases 135 | - Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments. Note: The manifest.json file must be in two places, first the root path of your repository and also in the release. 136 | - Publish the release. 137 | 138 | ### Adding your plugin to the community plugin list 139 | 140 | - Publish an initial version. 141 | - Make sure you have a `README.md` file in the root of your repo. 142 | - Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin. 143 | 144 | ### How to use 145 | 146 | - Clone this repo. 147 | - `npm i` or `yarn` to install dependencies 148 | - `npm run dev` to start compilation in watch mode. 149 | 150 | ### Manually installing the plugin 151 | 152 | - Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`. 153 | 154 | ### Improve code quality with eslint (optional) 155 | - [ESLint](https://eslint.org/) is a tool that analyzes your code to quickly find problems. You can run ESLint against your plugin to find common bugs and ways to improve your code. 156 | - To use eslint with this project, make sure to install eslint from terminal: 157 | - `npm install -g eslint` 158 | - To use eslint to analyze this project use this command: 159 | - `eslint main.ts` 160 | - eslint will then create a report with suggestions for code improvement by file and line number. 161 | - If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder: 162 | - `eslint .\src\` 163 | 164 | 165 | ### API Documentation 166 | 167 | See https://github.com/obsidianmd/obsidian-api 168 | --------------------------------------------------------------------------------