dragonish_obsidian-heading-.../components/editor.ts
2026-01-09 19:27:54 +08:00

306 lines
9.4 KiB
TypeScript

import { editorLivePreviewField } from "obsidian";
import {
ViewUpdate,
PluginValue,
EditorView,
DecorationSet,
Decoration,
} from "@codemirror/view";
import { RangeSetBuilder, StateEffect, StateField } from "@codemirror/state";
import { HeadingWidget } from "./weight";
import type { HeadingPluginData } from "../common/data";
import {
className,
getUnorderedLevelHeadings,
getOrderedCustomIdents,
findFirstCharacterIndex,
} from "../common/data";
import {
Querier,
UnorderedCounter,
OrderedCounter,
IndependentCounter,
SpliceCounter,
} from "../common/counter";
import { Heading } from "../common/heading";
/** A StateEffect for updating decorations */
const updateHeadingDecorations = StateEffect.define<DecorationSet>();
/** A StateField to manage the decorations */
export const headingDecorationsField = StateField.define<DecorationSet>({
create() {
return Decoration.none;
},
update(decorations, tr) {
for (const e of tr.effects) {
if (e.is(updateHeadingDecorations)) {
// Completely replace old decorations
return e.value;
}
}
return decorations.map(tr.changes);
},
provide: (f) => EditorView.decorations.from(f),
});
/** A StateEffect for editor mode */
export const updateEditorMode = StateEffect.define<boolean>();
/** A StateField to manage the editor mode */
export const editorModeField = StateField.define<boolean>({
create: () => false,
update(value, tr) {
for (const e of tr.effects) {
if (e.is(updateEditorMode)) {
return e.value;
}
}
return value;
},
});
export class HeadingEditorViewPlugin implements PluginValue {
getPluginData: () => Promise<HeadingPluginData>;
constructor(
view: EditorView,
getPluginData: () => Promise<HeadingPluginData>
) {
this.getPluginData = getPluginData;
this.updateDecorations(view, view.state.field(editorLivePreviewField));
}
update(update: ViewUpdate) {
if (
update.docChanged ||
update.viewportChanged ||
update.transactions.some((tr) =>
tr.effects.some((e) => e.is(updateEditorMode))
)
) {
this.updateDecorations(
update.view,
update.state.field(editorLivePreviewField)
);
}
}
destroy() {
// Cleanup if needed
}
private async updateDecorations(view: EditorView, isLivePreviwMode: boolean) {
const pluginData = await this.getPluginData();
if (
(isLivePreviwMode && pluginData.enabledInPreview) ||
(!isLivePreviwMode && pluginData.enabledInSource)
) {
//? Cache has latency, the level and position of the heading
//? object is not real-time and need self-calculation.
const builder = new RangeSetBuilder<Decoration>();
const doc = view.state.doc;
const {
decoratorMode = "orderd",
position,
opacity,
maxRecLevel,
orderedStyleType,
orderedDelimiter,
orderedTrailingDelimiter,
orderedCustomTrailingDelimiter,
orderedLeadingDelimiter,
orderedCustomLeadingDelimiter,
orderedCustomIdents,
orderedSpecifiedString,
orderedAlwaysIgnore,
orderedIgnoreSingle,
orderedIgnoreMaximum = 6,
orderedBasedOnExisting,
orderedAllowZeroLevel,
unorderedLevelHeadings,
independentSettings,
spliceSettings,
} = isLivePreviwMode
? pluginData.previewSettings
: pluginData.sourceSettings;
let counter: Counter;
if (decoratorMode === "unordered") {
counter = new UnorderedCounter(
getUnorderedLevelHeadings(unorderedLevelHeadings),
maxRecLevel
);
} else {
let ignoreTopLevel = 0;
const ignoreSingle = !orderedAlwaysIgnore && orderedIgnoreSingle;
const ignoreLimit = orderedAlwaysIgnore ? orderedIgnoreMaximum : 0;
if (ignoreSingle || orderedBasedOnExisting) {
const queier = new Querier(orderedAllowZeroLevel, maxRecLevel);
const heading = new Heading();
for (let lineIndex = 1; lineIndex <= doc.lines; lineIndex++) {
const line = doc.line(lineIndex);
const lineText = line.text;
const nextLineIndex = lineIndex + 1;
const nextLineText =
nextLineIndex <= doc.lines ? doc.line(nextLineIndex).text : "";
const level = heading.handler(lineIndex, lineText, nextLineText);
if (level === -1) {
continue;
}
queier.handler(level);
ignoreTopLevel = queier.query(ignoreSingle, orderedIgnoreMaximum);
if (ignoreTopLevel <= ignoreLimit) {
break;
}
}
}
if (ignoreTopLevel < ignoreLimit) {
ignoreTopLevel = ignoreLimit;
}
if (decoratorMode === "independent") {
counter = new IndependentCounter({
maxRecLevel,
ignoreTopLevel,
allowZeroLevel: orderedAllowZeroLevel,
orderedRecLevel: independentSettings?.orderedRecLevel,
h1: independentSettings?.h1,
h2: independentSettings?.h2,
h3: independentSettings?.h3,
h4: independentSettings?.h4,
h5: independentSettings?.h5,
h6: independentSettings?.h6,
});
} else if (decoratorMode === "splice") {
counter = new SpliceCounter({
maxRecLevel,
ignoreTopLevel,
allowZeroLevel: orderedAllowZeroLevel,
delimiter: spliceSettings?.delimiter,
trailingDelimiter: spliceSettings?.trailingDelimiter,
customTrailingDelimiter: spliceSettings?.customTrailingDelimiter,
leadingDelimiter: spliceSettings?.leadingDelimiter,
customLeadingDelimiter: spliceSettings?.customLeadingDelimiter,
h1: spliceSettings?.h1,
h2: spliceSettings?.h2,
h3: spliceSettings?.h3,
h4: spliceSettings?.h4,
h5: spliceSettings?.h5,
h6: spliceSettings?.h6,
});
} else {
counter = new OrderedCounter({
maxRecLevel,
ignoreTopLevel,
allowZeroLevel: orderedAllowZeroLevel,
styleType: orderedStyleType,
delimiter: orderedDelimiter,
trailingDelimiter: orderedTrailingDelimiter,
customTrailingDelimiter: orderedCustomTrailingDelimiter,
leadingDelimiter: orderedLeadingDelimiter,
customLeadingDelimiter: orderedCustomLeadingDelimiter,
customIdents: getOrderedCustomIdents(orderedCustomIdents),
specifiedString: orderedSpecifiedString,
});
}
}
const heading = new Heading();
for (let lineIndex = 1; lineIndex <= doc.lines; lineIndex++) {
const line = doc.line(lineIndex);
const lineText = line.text;
const nextLineIndex = lineIndex + 1;
const nextLineText =
nextLineIndex <= doc.lines ? doc.line(nextLineIndex).text : "";
const level = heading.handler(lineIndex, lineText, nextLineText);
if (level === -1) {
continue;
}
const content = counter.decorator(level);
if (content) {
const widget = new HeadingWidget(
isLivePreviwMode,
content,
opacity,
position,
level
);
const deco = Decoration.widget({
widget,
side: this.getSide(position),
block: false,
});
let hideDeco: Decoration | null = null;
if (!isLivePreviwMode) {
const hideNumberSigns = pluginData.sourceHideNumberSigns;
if (hideNumberSigns) {
hideDeco = Decoration.mark({
class: className.hideSourceNumberSigns,
});
}
}
if (position === "before-inside") {
const charIndex = isLivePreviwMode
? findFirstCharacterIndex(lineText)
: 0;
builder.add(line.from + charIndex, line.from + charIndex, deco);
if (hideDeco) {
const cIndex = findFirstCharacterIndex(lineText);
if (cIndex > 0) {
builder.add(line.from, line.from + cIndex, hideDeco);
}
}
} else if (position === "before") {
builder.add(line.from, line.from, deco);
if (hideDeco) {
const cIndex = findFirstCharacterIndex(lineText);
if (cIndex > 0) {
builder.add(line.from, line.from + cIndex, hideDeco);
}
}
} else {
if (hideDeco) {
const cIndex = findFirstCharacterIndex(lineText);
if (cIndex > 0) {
builder.add(line.from, line.from + cIndex, hideDeco);
}
}
builder.add(line.to, line.to, deco);
}
}
}
const newDecorations = builder.finish();
view.dispatch({
effects: updateHeadingDecorations.of(newDecorations),
});
} else {
// Clear decorations if not enabled in the current mode
view.dispatch({
effects: updateHeadingDecorations.of(Decoration.none),
});
}
}
private getSide(position: PostionOptions): number {
if (position.includes("before")) {
return position.includes("inside") ? 1 : -1;
} else {
return position.includes("inside") ? -1 : 1;
}
}
}