2025-03-10 11:04:36 +00:00
|
|
|
import { editorLivePreviewField } from "obsidian";
|
|
|
|
|
import {
|
|
|
|
|
ViewUpdate,
|
|
|
|
|
PluginValue,
|
|
|
|
|
EditorView,
|
|
|
|
|
DecorationSet,
|
|
|
|
|
Decoration,
|
|
|
|
|
} from "@codemirror/view";
|
|
|
|
|
import { RangeSetBuilder, StateEffect, StateField } from "@codemirror/state";
|
2025-04-09 13:45:01 +00:00
|
|
|
import { HeadingWidget } from "./weight";
|
2025-03-10 11:04:36 +00:00
|
|
|
import type { HeadingPluginData } from "../common/data";
|
|
|
|
|
import {
|
2026-01-05 09:29:36 +00:00
|
|
|
className,
|
2025-03-11 05:08:06 +00:00
|
|
|
getUnorderedLevelHeadings,
|
|
|
|
|
getOrderedCustomIdents,
|
2025-04-09 13:45:01 +00:00
|
|
|
findFirstCharacterIndex,
|
2025-03-10 11:04:36 +00:00
|
|
|
} from "../common/data";
|
2026-01-09 05:04:07 +00:00
|
|
|
import {
|
|
|
|
|
Querier,
|
|
|
|
|
UnorderedCounter,
|
|
|
|
|
OrderedCounter,
|
|
|
|
|
IndependentCounter,
|
|
|
|
|
} from "../common/counter";
|
2025-03-13 02:15:02 +00:00
|
|
|
import { Heading } from "../common/heading";
|
2025-03-10 11:04:36 +00:00
|
|
|
|
|
|
|
|
/** 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;
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-28 13:02:52 +00:00
|
|
|
export class HeadingEditorViewPlugin implements PluginValue {
|
2025-03-10 11:04:36 +00:00
|
|
|
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 ||
|
2025-03-10 13:51:19 +00:00
|
|
|
update.transactions.some((tr) =>
|
|
|
|
|
tr.effects.some((e) => e.is(updateEditorMode))
|
|
|
|
|
)
|
2025-03-10 11:04:36 +00:00
|
|
|
) {
|
|
|
|
|
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)
|
|
|
|
|
) {
|
2025-03-13 02:15:02 +00:00
|
|
|
//? Cache has latency, the level and position of the heading
|
|
|
|
|
//? object is not real-time and need self-calculation.
|
2025-03-10 11:04:36 +00:00
|
|
|
const builder = new RangeSetBuilder<Decoration>();
|
|
|
|
|
const doc = view.state.doc;
|
|
|
|
|
|
|
|
|
|
const {
|
2026-01-09 05:04:07 +00:00
|
|
|
decoratorMode = "orderd",
|
2025-03-10 11:04:36 +00:00
|
|
|
position,
|
|
|
|
|
opacity,
|
2025-08-10 13:55:41 +00:00
|
|
|
maxRecLevel,
|
2025-03-10 11:04:36 +00:00
|
|
|
orderedStyleType,
|
|
|
|
|
orderedDelimiter,
|
|
|
|
|
orderedTrailingDelimiter,
|
2025-08-10 12:09:28 +00:00
|
|
|
orderedCustomTrailingDelimiter,
|
2025-08-16 02:25:31 +00:00
|
|
|
orderedLeadingDelimiter,
|
|
|
|
|
orderedCustomLeadingDelimiter,
|
2025-03-10 11:04:36 +00:00
|
|
|
orderedCustomIdents,
|
|
|
|
|
orderedSpecifiedString,
|
2025-06-20 12:48:02 +00:00
|
|
|
orderedAlwaysIgnore,
|
2025-03-10 14:45:56 +00:00
|
|
|
orderedIgnoreSingle,
|
2025-04-02 13:51:54 +00:00
|
|
|
orderedIgnoreMaximum = 6,
|
2025-03-27 13:19:28 +00:00
|
|
|
orderedBasedOnExisting,
|
2025-03-12 14:20:38 +00:00
|
|
|
orderedAllowZeroLevel,
|
2025-03-10 11:04:36 +00:00
|
|
|
unorderedLevelHeadings,
|
2026-01-09 05:04:07 +00:00
|
|
|
independentSettings,
|
2025-03-11 05:08:06 +00:00
|
|
|
} = isLivePreviwMode
|
|
|
|
|
? pluginData.previewSettings
|
|
|
|
|
: pluginData.sourceSettings;
|
2025-03-10 11:04:36 +00:00
|
|
|
|
2026-01-09 05:04:07 +00:00
|
|
|
let counter: Counter;
|
|
|
|
|
if (decoratorMode === "unordered") {
|
|
|
|
|
counter = new UnorderedCounter(
|
|
|
|
|
getUnorderedLevelHeadings(unorderedLevelHeadings),
|
|
|
|
|
maxRecLevel
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
let ignoreTopLevel = 0;
|
2025-06-20 12:48:02 +00:00
|
|
|
const ignoreSingle = !orderedAlwaysIgnore && orderedIgnoreSingle;
|
|
|
|
|
const ignoreLimit = orderedAlwaysIgnore ? orderedIgnoreMaximum : 0;
|
|
|
|
|
if (ignoreSingle || orderedBasedOnExisting) {
|
2025-08-10 13:55:41 +00:00
|
|
|
const queier = new Querier(orderedAllowZeroLevel, maxRecLevel);
|
2025-06-20 12:48:02 +00:00
|
|
|
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;
|
|
|
|
|
}
|
2025-03-27 13:19:28 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-06-20 12:48:02 +00:00
|
|
|
if (ignoreTopLevel < ignoreLimit) {
|
|
|
|
|
ignoreTopLevel = ignoreLimit;
|
|
|
|
|
}
|
2025-03-10 14:45:56 +00:00
|
|
|
|
2026-01-09 05:04:07 +00:00
|
|
|
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 {
|
|
|
|
|
counter = new OrderedCounter({
|
|
|
|
|
maxRecLevel,
|
|
|
|
|
ignoreTopLevel,
|
|
|
|
|
allowZeroLevel: orderedAllowZeroLevel,
|
|
|
|
|
styleType: orderedStyleType,
|
|
|
|
|
delimiter: orderedDelimiter,
|
|
|
|
|
trailingDelimiter: orderedTrailingDelimiter,
|
|
|
|
|
customTrailingDelimiter: orderedCustomTrailingDelimiter,
|
|
|
|
|
leadingDelimiter: orderedLeadingDelimiter,
|
|
|
|
|
customLeadingDelimiter: orderedCustomLeadingDelimiter,
|
|
|
|
|
customIdents: getOrderedCustomIdents(orderedCustomIdents),
|
|
|
|
|
specifiedString: orderedSpecifiedString,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-10 11:04:36 +00:00
|
|
|
|
2025-03-13 02:15:02 +00:00
|
|
|
const heading = new Heading();
|
2025-03-14 05:23:53 +00:00
|
|
|
for (let lineIndex = 1; lineIndex <= doc.lines; lineIndex++) {
|
2025-03-10 11:04:36 +00:00
|
|
|
const line = doc.line(lineIndex);
|
|
|
|
|
const lineText = line.text;
|
2025-03-13 02:15:02 +00:00
|
|
|
const nextLineIndex = lineIndex + 1;
|
|
|
|
|
const nextLineText =
|
2025-03-14 05:23:53 +00:00
|
|
|
nextLineIndex <= doc.lines ? doc.line(nextLineIndex).text : "";
|
2025-03-13 02:15:02 +00:00
|
|
|
const level = heading.handler(lineIndex, lineText, nextLineText);
|
2025-03-10 11:04:36 +00:00
|
|
|
if (level === -1) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-12 07:57:17 +00:00
|
|
|
const content = counter.decorator(level);
|
2025-03-10 11:04:36 +00:00
|
|
|
|
2025-03-27 14:00:13 +00:00
|
|
|
if (content) {
|
2025-04-18 15:35:39 +00:00
|
|
|
const widget = new HeadingWidget(
|
|
|
|
|
isLivePreviwMode,
|
|
|
|
|
content,
|
|
|
|
|
opacity,
|
2026-01-06 12:47:37 +00:00
|
|
|
position,
|
|
|
|
|
level
|
2025-04-18 15:35:39 +00:00
|
|
|
);
|
|
|
|
|
const deco = Decoration.widget({
|
|
|
|
|
widget,
|
|
|
|
|
side: this.getSide(position),
|
|
|
|
|
block: false,
|
|
|
|
|
});
|
|
|
|
|
|
2025-04-27 13:36:17 +00:00
|
|
|
let hideDeco: Decoration | null = null;
|
|
|
|
|
if (!isLivePreviwMode) {
|
2025-08-05 14:22:22 +00:00
|
|
|
const hideNumberSigns = pluginData.sourceHideNumberSigns;
|
2025-04-27 13:36:17 +00:00
|
|
|
if (hideNumberSigns) {
|
|
|
|
|
hideDeco = Decoration.mark({
|
2026-01-05 09:29:36 +00:00
|
|
|
class: className.hideSourceNumberSigns,
|
2025-04-27 13:36:17 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-18 15:35:39 +00:00
|
|
|
if (position === "before-inside") {
|
|
|
|
|
const charIndex = isLivePreviwMode
|
|
|
|
|
? findFirstCharacterIndex(lineText)
|
|
|
|
|
: 0;
|
|
|
|
|
builder.add(line.from + charIndex, line.from + charIndex, deco);
|
2025-04-27 13:36:17 +00:00
|
|
|
|
|
|
|
|
if (hideDeco) {
|
|
|
|
|
const cIndex = findFirstCharacterIndex(lineText);
|
|
|
|
|
if (cIndex > 0) {
|
|
|
|
|
builder.add(line.from, line.from + cIndex, hideDeco);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-18 15:35:39 +00:00
|
|
|
} else if (position === "before") {
|
2025-04-09 13:45:01 +00:00
|
|
|
builder.add(line.from, line.from, deco);
|
2025-04-27 13:36:17 +00:00
|
|
|
|
|
|
|
|
if (hideDeco) {
|
|
|
|
|
const cIndex = findFirstCharacterIndex(lineText);
|
|
|
|
|
if (cIndex > 0) {
|
|
|
|
|
builder.add(line.from, line.from + cIndex, hideDeco);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-18 15:35:39 +00:00
|
|
|
} else {
|
2025-04-27 13:36:17 +00:00
|
|
|
if (hideDeco) {
|
|
|
|
|
const cIndex = findFirstCharacterIndex(lineText);
|
|
|
|
|
if (cIndex > 0) {
|
|
|
|
|
builder.add(line.from, line.from + cIndex, hideDeco);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-18 15:35:39 +00:00
|
|
|
builder.add(line.to, line.to, deco);
|
2025-04-09 13:45:01 +00:00
|
|
|
}
|
2025-03-27 14:00:13 +00:00
|
|
|
}
|
2025-03-10 11:04:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-18 15:35:39 +00:00
|
|
|
|
|
|
|
|
private getSide(position: PostionOptions): number {
|
|
|
|
|
if (position.includes("before")) {
|
|
|
|
|
return position.includes("inside") ? 1 : -1;
|
|
|
|
|
} else {
|
|
|
|
|
return position.includes("inside") ? -1 : 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-10 11:04:36 +00:00
|
|
|
}
|