#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);
}
};
}
```