From f5a3047417eadb9940772866d7805a0062c9d197 Mon Sep 17 00:00:00 2001 From: Sayuop Date: Wed, 26 Nov 2025 10:42:35 +0100 Subject: [PATCH] Add Live Preview support and reorder counter elements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move buttons to right side of number (number, -, +, label) - Add CodeMirror ViewPlugin for Live Preview rendering - Counter now works in edit mode (Live Preview) - Create CounterWidget for editor decorations - Use Decoration.replace to render counters in editor 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- main.ts | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/main.ts b/main.ts index 7f42b43..9b457fb 100644 --- a/main.ts +++ b/main.ts @@ -1,4 +1,42 @@ import { Plugin, MarkdownPostProcessorContext, MarkdownView } from 'obsidian'; +import { Decoration, DecorationSet, EditorView, WidgetType, ViewPlugin, ViewUpdate } from '@codemirror/view'; +import { RangeSetBuilder } from '@codemirror/state'; + +class CounterWidget extends WidgetType { + constructor(private value: number, private label: string, private plugin: CounterPlugin) { + super(); + } + + toDOM(view: EditorView): HTMLElement { + return this.plugin.createCounterElement(this.value, this.label, {} as MarkdownPostProcessorContext); + } +} + +function buildCounterDecorations(view: EditorView, plugin: CounterPlugin): DecorationSet { + const builder = new RangeSetBuilder(); + const counterRegex = /~\s*\(\s*(\d*)\s*\)\s*(.+)/g; + + for (let { from, to } of view.visibleRanges) { + const text = view.state.doc.sliceString(from, to); + let match; + counterRegex.lastIndex = 0; + + while ((match = counterRegex.exec(text)) !== null) { + const startPos = from + match.index; + const endPos = startPos + match[0].length; + const value = match[1] === '' ? 0 : parseInt(match[1], 10); + const label = match[2].trim(); + + const widget = Decoration.replace({ + widget: new CounterWidget(value, label, plugin), + }); + + builder.add(startPos, endPos, widget); + } + } + + return builder.finish(); +} export default class CounterPlugin extends Plugin { async onload() { @@ -7,6 +45,28 @@ export default class CounterPlugin extends Plugin { this.registerMarkdownPostProcessor((element, context) => { this.processCounters(element, context); }); + + const plugin = this; + this.registerEditorExtension( + ViewPlugin.fromClass( + class { + decorations: DecorationSet; + + constructor(view: EditorView) { + this.decorations = buildCounterDecorations(view, plugin); + } + + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged) { + this.decorations = buildCounterDecorations(update.view, plugin); + } + } + }, + { + decorations: (v) => v.decorations, + } + ) + ); } onunload() { @@ -147,8 +207,8 @@ export default class CounterPlugin extends Plugin { updateSource(currentValue); }); - container.appendChild(minusButton); container.appendChild(counterDisplay); + container.appendChild(minusButton); container.appendChild(plusButton); if (label) { container.appendChild(labelSpan);