CodeMirror 6 扩展开发
Obsidian 的编辑器基于 CodeMirror 6 构建。通过开发 CodeMirror 6 扩展,你可以深度定制编辑体验——添加语法高亮、自定义装饰、键盘快捷键等。
CodeMirror 6 架构
核心概念
| 概念 | 说明 |
|---|---|
| State | 编辑器的不可变状态对象 |
| View | 编辑器的视图层 |
| Extension | 扩展编辑器功能的配置单元 |
| Facet | 扩展间的通信机制 |
| Effect | 状态变更的消息 |
| Decoration | 视觉装饰(高亮、小部件等) |
| Command | 可执行的编辑操作 |
扩展类型
text
CodeMirror 6 扩展
├── 语法扩展 (Language)
│ ├── 自定义语法高亮
│ └── 代码折叠
├── 装饰扩展 (Decoration)
│ ├── 行内装饰(高亮、下划线)
│ ├── 替换装饰(替换文本显示)
│ └── 挂件装饰(在文本中嵌入元素)
├── 命令扩展 (Command)
│ ├── 键盘快捷键
│ └── 命令面板命令
├── 状态扩展 (StateField)
│ ├── 自定义状态
│ └── 状态持久化
└── 视图插件 (ViewPlugin)
├── 响应视图更新
└── 管理 DOM 元素在 Obsidian 中注册扩展
使用 EditorView 扩展
typescript
// main.ts
import { Plugin } from "obsidian";
import { myExtension } from "./codemirror/myExtension";
export default class MyPlugin extends Plugin {
async onload() {
// 注册 CodeMirror 6 扩展
this.registerEditorExtension(myExtension);
}
}条件注册
typescript
export default class MyPlugin extends Plugin {
async onload() {
// 根据设置条件注册
this.registerEditorExtension(
this.settings.enableHighlight ? myHighlightExtension : []
);
// 动态切换扩展
this.addSettingTab(new MySettingTab(this.app, this));
}
updateExtensions() {
// 重新注册扩展以应用设置变更
this.app.workspace.updateOptions();
}
}装饰扩展
装饰(Decoration)是 CodeMirror 6 中最常用的扩展类型,用于改变文本的视觉呈现。
创建装饰集
typescript
import {
Decoration,
DecorationSet,
EditorView,
ViewPlugin,
ViewUpdate,
Range,
} from "@codemirror/view";
import { EditorState, RangeSetBuilder } from "@codemirror/state";
// 定义装饰类型
const highlightDecoration = Decoration.mark({
class: "cm-my-highlight",
attributes: { style: "background-color: rgba(255, 200, 0, 0.3)" },
});
const errorDecoration = Decoration.mark({
class: "cm-my-error",
attributes: { style: "text-decoration: wavy underline red" },
});
const widgetDecoration = (text: string) =>
Decoration.widget({
widget: new MyWidget(text),
});
class MyWidget extends WidgetType {
constructor(readonly text: string) {
super();
}
toDOM(): HTMLElement {
const span = document.createElement("span");
span.className = "cm-my-widget";
span.textContent = this.text;
return span;
}
}创建视图插件
typescript
const highlightPlugin = ViewPlugin.fromClass(
class {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = this.buildDecorations(view);
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = this.buildDecorations(update.view);
}
}
buildDecorations(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
const doc = view.state.doc;
for (const { from, to } of view.visibleRanges) {
// 从可见范围中扫描文本
const text = doc.sliceString(from, to);
// 示例:高亮所有 TODO 注释
const regex = /TODO:.*$/gm;
let match;
while ((match = regex.exec(text)) !== null) {
const start = from + match.index;
const end = start + match[0].length;
builder.add(start, end, highlightDecoration);
}
}
return builder.finish();
}
},
{
decorations: (v) => v.decorations,
}
);
export const myHighlightExtension = [highlightPlugin];行装饰
typescript
const activeLineDecoration = Decoration.line({
class: "cm-active-line-highlight",
});
const lineHighlightPlugin = ViewPlugin.fromClass(
class {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = this.buildDecorations(view);
}
update(update: ViewUpdate) {
this.decorations = this.buildDecorations(update.view);
}
buildDecorations(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
const cursorLine = view.state.doc.lineAt(
view.state.selection.main.head
);
builder.add(cursorLine.from, cursorLine.from, activeLineDecoration);
return builder.finish();
}
},
{
decorations: (v) => v.decorations,
}
);替换装饰
将文本替换为自定义显示(不影响实际内容):
typescript
const hideMarkerDecoration = Decoration.replace({
widget: new HiddenMarkerWidget(),
});
class HiddenMarkerWidget extends WidgetType {
toDOM(): HTMLElement {
const span = document.createElement("span");
span.className = "cm-hidden-marker";
span.textContent = "···";
span.style.cssText = "color: var(--text-faint); font-style: italic;";
return span;
}
get estimatedHeight(): number {
return -1;
}
ignoreEvent(): boolean {
return false;
}
}状态字段
StateField 用于在编辑器中存储自定义状态。
基本用法
typescript
import { StateField, StateEffect } from "@codemirror/state";
// 定义效果(用于修改状态)
const setCounter = StateEffect.define<number>();
// 定义状态字段
const counterField = StateField.define<number>({
create: () => 0,
update: (value, transaction) => {
for (const effect of transaction.effects) {
if (effect.is(setCounter)) {
return effect.value;
}
}
return value;
},
});
// 使用效果修改状态
function incrementCounter(view: EditorView) {
const current = view.state.field(counterField);
view.dispatch({
effects: setCounter.of(current + 1),
});
}在视图中使用状态字段
typescript
const counterDisplay = ViewPlugin.fromClass(
class {
dom: HTMLElement;
constructor(readonly view: EditorView) {
this.dom = document.createElement("div");
this.dom.className = "cm-counter-display";
this.updateDisplay();
}
update(update: ViewUpdate) {
if (update.state.field(counterField) !== update.startState.field(counterField)) {
this.updateDisplay();
}
}
updateDisplay() {
const count = this.view.state.field(counterField);
this.dom.textContent = `Count: ${count}`;
}
destroy() {
this.dom.remove();
}
}
);键盘快捷键
添加键绑定
typescript
import { keymap, EditorView } from "@codemirror/view";
import { Prec } from "@codemirror/state";
const myKeymap = keymap.of([
{
key: "Mod-Shift-h",
run: (view) => {
// 在当前光标位置插入文本
const cursor = view.state.selection.main.head;
view.dispatch({
changes: { from: cursor, insert: "Hello World" },
});
return true;
},
},
{
key: "Mod-Shift-t",
run: (view) => {
// 在当前行添加 TODO 标记
const line = view.state.doc.lineAt(view.state.selection.main.head);
view.dispatch({
changes: { from: line.from, insert: "TODO: " },
});
return true;
},
},
]);
// 使用高优先级确保快捷键生效
export const myKeymapExtension = Prec.high(myKeymap);常用快捷键修饰符
| 修饰符 | 含义 |
|---|---|
Mod | macOS: Cmd, 其他: Ctrl |
Alt | macOS: Option, 其他: Alt |
Shift | Shift |
Ctrl | Ctrl |
Cmd | Cmd (macOS) |
快捷键冲突处理
typescript
// 使用 Prec 控制优先级
import { Prec } from "@codemirror/state";
// 最高优先级
const highPriority = Prec.high(keymap.of([...]));
// 默认优先级
const normalPriority = keymap.of([...]);
// 最低优先级
const lowPriority = Prec.low(keymap.of([...]));语法高亮
使用 Lezer 解析器
CodeMirror 6 使用 Lezer 解析器系统进行语法分析:
typescript
import { syntaxTree } from "@codemirror/language";
import { RangeSetBuilder } from "@codemirror/state";
const syntaxHighlightPlugin = ViewPlugin.fromClass(
class {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = this.buildDecorations(view);
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = this.buildDecorations(update.view);
}
}
buildDecorations(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
const doc = view.state.doc;
for (const { from, to } of view.visibleRanges) {
syntaxTree(view.state).iterate({
from,
to,
enter: (node) => {
// 根据语法节点类型添加装饰
switch (node.name) {
case "HeaderMark":
builder.add(
node.from,
node.to,
Decoration.mark({ class: "cm-header-mark" })
);
break;
case "Emphasis":
builder.add(
node.from,
node.to,
Decoration.mark({ class: "cm-emphasis" })
);
break;
}
},
});
}
return builder.finish();
}
},
{
decorations: (v) => v.decorations,
}
);完整示例:TODO 高亮插件
typescript
// src/codemirror/todoHighlight.ts
import {
Decoration,
DecorationSet,
EditorView,
ViewPlugin,
ViewUpdate,
} from "@codemirror/view";
import { RangeSetBuilder } from "@codemirror/state";
// 定义装饰类型
const todoDecoration = Decoration.mark({
class: "cm-todo-highlight",
});
const doneDecoration = Decoration.mark({
class: "cm-done-highlight",
});
// 创建插件
const todoHighlightPlugin = ViewPlugin.fromClass(
class {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = this.buildDecorations(view);
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = this.buildDecorations(update.view);
}
}
buildDecorations(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
const doc = view.state.doc;
for (const { from, to } of view.visibleRanges) {
const text = doc.sliceString(from, to);
const lines = text.split("\n");
let offset = 0;
for (const line of lines) {
const lineStart = from + offset;
// 匹配未完成的 TODO
const todoMatch = line.match(/TODO:/);
if (todoMatch) {
const start = lineStart + todoMatch.index!;
builder.add(start, lineStart + line.length, todoDecoration);
}
// 匹配已完成的 DONE
const doneMatch = line.match(/DONE:/);
if (doneMatch) {
const start = lineStart + doneMatch.index!;
builder.add(start, lineStart + line.length, doneDecoration);
}
offset += line.length + 1; // +1 for newline
}
}
return builder.finish();
}
},
{
decorations: (v) => v.decorations,
}
);
export const todoHighlightExtension = [
todoHighlightPlugin,
// 配套 CSS
EditorView.baseTheme({
".cm-todo-highlight": {
backgroundColor: "rgba(255, 200, 0, 0.15)",
borderLeft: "3px solid #ffc800",
},
".cm-done-highlight": {
backgroundColor: "rgba(0, 200, 100, 0.1)",
borderLeft: "3px solid #00c864",
textDecoration: "line-through",
opacity: "0.7",
},
}),
];在插件中注册:
typescript
// main.ts
import { Plugin } from "obsidian";
import { todoHighlightExtension } from "./codemirror/todoHighlight";
export default class TodoHighlightPlugin extends Plugin {
async onload() {
this.registerEditorExtension(todoHighlightExtension);
}
}调试技巧
查看语法树
typescript
// 在开发者控制台中
const view = app.workspace.activeLeaf.view;
const tree = syntaxTree(view.editor.cm.state);
tree.iterate({
enter: (node) => console.log(node.name, node.from, node.to),
});检查装饰集
typescript
// 查看当前装饰
const view = app.workspace.activeLeaf.view;
const decorations = view.editor.cm.plugin(highlightPlugin)?.decorations;
decorations?.between(0, view.editor.cm.state.doc.length, (from, to, dec) => {
console.log(from, to, dec);
});性能建议
- 只处理可见范围:使用
view.visibleRanges而非整个文档 - 缓存装饰:只在文档变化或视口变化时重建
- 使用 RangeSetBuilder:比手动创建 DecorationSet 更高效
- 避免频繁 dispatch:合并多个变更在一次 dispatch 中
- 精简 DOM 操作:WidgetType 的
toDOM应尽量简单
相关资源
- 插件开发入门 — 开发环境搭建
- API 参考 — Obsidian API 文档
- CodeMirror 6 文档 — 官方指南
- CodeMirror 6 API — API 参考
- Lezer 解析器 — 语法解析系统