#Obsidian ## 経緯 [[📜Obsidian jump-to-linkプラグインをLive Preview対応してみる]]試みをしたけど挫折したのでリベンジする。ただ、今後のことも考えて1から[[CodeMirror 6]]を勉強しつつ作ってみる。 ## [[CodeMirror 6]]を使って[[DOM]]を更新する [CodeMirror Decoration Example](https://codemirror.net/6/examples/decoration/) をコピペする。 ```ts:main.ts import { Plugin } from "obsidian"; import { AppHelper } from "./app-helper"; import { Decoration, DecorationSet, EditorView } from "@codemirror/view"; import { StateEffect, StateField } from "@codemirror/state"; export default class QuickJumpPlugin extends Plugin { async onload() { const appHelper = new AppHelper(this.app); this.addCommand({ id: "open-sample-modal-complex", name: "Open sample modal (complex)", checkCallback: (checking: boolean) => { const markdownView = appHelper.getMarkdownViewInActiveLeaf(); if (markdownView?.getMode() === "source") { if (!checking) { const cmEditorView: EditorView = (markdownView.editor as any).cm; underlineSelection(cmEditorView); } return true; } }, }); } onunload() {} } //----------------- ここからコピペ const addUnderline = StateEffect.define<{ from: number; to: number }>(); const underlineField = StateField.define<DecorationSet>({ create() { return Decoration.none; }, update(underlines, tr) { console.log("update"); underlines = underlines.map(tr.changes); for (let e of tr.effects) if (e.is(addUnderline)) { underlines = underlines.update({ add: [underlineMark.range(e.value.from, e.value.to)], }); } return underlines; }, provide: (f) => EditorView.decorations.from(f), }); const underlineMark = Decoration.mark({ class: "cm-underline" }); const underlineTheme = EditorView.baseTheme({ ".cm-underline": { textDecoration: "underline 3px red" }, }); export function underlineSelection(view: EditorView) { let effects: StateEffect<unknown>[] = view.state.selection.ranges .filter((r) => !r.empty) .map(({ from, to }) => addUnderline.of({ from, to })); if (!effects.length) return false; if (!view.state.field(underlineField, false)) effects.push(StateEffect.appendConfig.of([underlineField, underlineTheme])); view.dispatch({ effects }); return true; } ``` コマンドを実行すると[[DOM]]が更新されることを確認した。 ### ラベルっぽいものを表示してみる Mark decorationsだと厳しそう。。ラベル内の文字表示を切り分ける方法で断念 ```ts const label = StateEffect.define<{ from: number; to: number; mark: string }>(); const labelMark = Decoration.mark({ class: "quick-jump-cm-label", }); const labelTheme = EditorView.baseTheme({ ".quick-jump-cm-label": { textDecoration: "underline 3px red", }, ".quick-jump-cm-label:before": { content: "'A'", backgroundColor: "red", padding: "3px", }, }); const labelField = StateField.define<DecorationSet>({ create() { return Decoration.none; }, update(labels, tr) { labels = labels.map(tr.changes); for (let e of tr.effects) if (e.is(label)) { labels = labels.update({ add: [labelMark.range(e.value.from, e.value.to)], }); } return labels; }, provide: (f) => EditorView.decorations.from(f), }); export function selectLabel(view: EditorView) { const effects: StateEffect<unknown>[] = view.state.selection.ranges .filter((r) => !r.empty) .map(({ from, to }) => label.of({ from, to, mark: "B" })); if (!effects.length) { return false; } if (!view.state.field(labelField, false)) { effects.push(StateEffect.appendConfig.of([labelField, labelTheme])); } view.dispatch({ effects }); return true; } ``` ### Decorationで一旦動くもの Decorationを使うことで一旦ラベル表示まではできるようになった。が一度表示した後に削除ができない。 ### Extensionを登録する 調べていくと[[Obsidian]]に[[Plugin_2.registerEditorExtension]]という関数があった。 ```ts declare type Extension = { extension: Extension; } | readonly Extension[]; ``` ただ、`Extension`ってただの再帰にしか見えない。。 とりあえず、`ESC`で消せるようになった!!!; ```ts import { Plugin } from "obsidian"; import { AppHelper } from "./app-helper"; import { ViewPlugin } from "@codemirror/view"; import { Extension } from "@codemirror/state"; import { createViewPluginClass, MarkPlugin } from "./plugins/MarkPlugin"; export default class QuickJumpPlugin extends Plugin { extensions: Extension[]; async onload() { const appHelper = new AppHelper(this.app); const markPlugin = new MarkPlugin(appHelper); const markViewPlugin = ViewPlugin.fromClass( createViewPluginClass(markPlugin), { decorations: (v) => v.decorations, } ); this.addCommand({ id: "open-sample-modal-complex", name: "Open sample modal (complex)", checkCallback: (checking: boolean) => { const markdownView = appHelper.getMarkdownViewInActiveLeaf(); if (markdownView?.getMode() === "source") { if (!checking) { this.extensions = [markViewPlugin]; this.registerEditorExtension(this.extensions); // 実は不要か...? // const cmEditorView: EditorView = (markdownView.editor as any).cm; // cmEditorView.dispatch({ // effects: StateEffect.appendConfig.of([markViewPlugin]), // }); this.app.scope.register([], "Escape", () => { this.extensions.remove(markViewPlugin); this.app.workspace.updateOptions(); // 頑張っていたけど意味ない? // cmEditorView.dispatch({ // effects: StateEffect.reconfigure.of(cmEditorView.state), // }); }); } return true; } }, }); } onunload() {} } ``` ## トラブル解消 ### Viewport周りの挙動が怪しい ViewPluginClassの`update`で条件を抜いたら動いた。。 ```ts:対応前 export function createViewPluginClass(markPlugin: MarkPlugin) { return class { decorations: DecorationSet; constructor(view: EditorView) { this.decorations = markPlugin.createMarks(view); } update(update: ViewUpdate) { // ここの判定で色々予期せぬ動きをしてたぽい (サンプルコードそのまま) if (update.docChanged || update.viewportChanged) { this.decorations = markPlugin.createMarks(update.view); } } }; } ``` ```ts:対応後 export function createViewPluginClass(markPlugin: MarkPlugin) { return class { decorations: DecorationSet; constructor(view: EditorView) { this.decorations = markPlugin.createMarks(view); } update(update: ViewUpdate) { this.decorations = markPlugin.createMarks(update.view); } }; } ```