diff --git a/package.json b/package.json index 6d9d916..c49defb 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,9 @@ "@types/node": "^16.11.6", "@typescript-eslint/eslint-plugin": "5.29.0", "@typescript-eslint/parser": "5.29.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@codemirror/language": "^6.0.0", "builtin-modules": "3.3.0", "dotenv": "^16.5.0", "dotenv-expand": "^12.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e7e7a3..f420e87 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,15 @@ importers: .: devDependencies: + '@codemirror/language': + specifier: ^6.0.0 + version: 6.11.1 + '@codemirror/state': + specifier: ^6.0.0 + version: 6.5.2 + '@codemirror/view': + specifier: ^6.0.0 + version: 6.37.1 '@types/node': specifier: ^16.11.6 version: 16.18.126 @@ -41,6 +50,9 @@ importers: packages: + '@codemirror/language@6.11.1': + resolution: {integrity: sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==} + '@codemirror/state@6.5.2': resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} @@ -210,6 +222,15 @@ packages: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead + '@lezer/common@1.2.3': + resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} + + '@lezer/highlight@1.2.1': + resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==} + + '@lezer/lr@1.4.2': + resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==} + '@marijn/find-cluster-break@1.0.2': resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} @@ -774,6 +795,15 @@ packages: snapshots: + '@codemirror/language@6.11.1': + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.37.1 + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + style-mod: 4.1.2 + '@codemirror/state@6.5.2': dependencies: '@marijn/find-cluster-break': 1.0.2 @@ -886,6 +916,16 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} + '@lezer/common@1.2.3': {} + + '@lezer/highlight@1.2.1': + dependencies: + '@lezer/common': 1.2.3 + + '@lezer/lr@1.4.2': + dependencies: + '@lezer/common': 1.2.3 + '@marijn/find-cluster-break@1.0.2': {} '@nodelib/fs.scandir@2.1.5': diff --git a/src/main.ts b/src/main.ts index 7f37f59..0a0d76b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,42 +1,146 @@ import { MarkdownView, Plugin, Notice, Editor, EditorPosition } from 'obsidian'; +import { hackToRerender } from './utils/editorUtils'; import { TextInputModal } from './modals/TextInputModal'; +// Import CodeMirror modules directly as per Obsidian documentation +import { ViewUpdate, PluginValue, EditorView, ViewPlugin, Decoration, DecorationSet } from '@codemirror/view'; +import { RangeSetBuilder, StateField, Extension } from '@codemirror/state'; + export default class VariantEditor extends Plugin { + // Track active line for dimming + private activeLine: number | null = null; + // Store the extension to update it later + private dimExtension: Extension | null = null; + // Track previous cursor line + private previousCursorLine: number | null = null; async onload() { - console.log('VariantEditor: Plugin loading...'); - try { // Bind the method to ensure proper 'this' context this.highlightSelection = this.highlightSelection.bind(this); + this.clearHighlight = this.clearHighlight.bind(this); // Register the command with a direct function reference this.addCommand({ id: 'variant-editor-highlight', name: 'Variant Editor: Highlight Word & Sentence', hotkeys: [{ modifiers: ["Mod"], key: "h" }], - callback: () => { - console.log('VariantEditor: Command callback executed'); - this.highlightSelection(); - } + callback: () => this.highlightSelection() }); - } catch (error) { - console.error('VariantEditor: Error during initialization:', error); + // Register the clear command + this.addCommand({ + id: 'variant-editor-clear-highlight', + name: 'Variant Editor: Clear Highlighting', + callback: () => this.clearHighlight() + }); + + // Register the editor extension for dimming + this.dimExtension = this.createDimExtension(); + this.registerEditorExtension(this.dimExtension); + } catch (e) { + console.error('Error during initialization:', e); } } onunload() { this.clearHighlight(); } + + private createDimExtension() { + // Get reference to the plugin instance for the view plugin to use + const pluginInstance = this; + + // Create a state field to track cursor position changes + const cursorTrackingField = StateField.define({ + create(state) { + return null; + }, + update(value, transaction) { + if (transaction.selection) { + const selection = transaction.newSelection.main; + const currentLine = transaction.newDoc.lineAt(selection.head).number; + + // Store the current cursor line + if (pluginInstance.previousCursorLine === null) { + pluginInstance.previousCursorLine = currentLine; + } + + // If we have an active line and cursor moved to a different line, clear dimming + if (pluginInstance.activeLine !== null && + currentLine !== pluginInstance.activeLine && + currentLine !== pluginInstance.previousCursorLine) { + // Schedule clearing on next tick to avoid update-during-update + setTimeout(() => { + pluginInstance.clearHighlight(); + }, 0); + } + + // Update previous cursor line + pluginInstance.previousCursorLine = currentLine; + } + return value; + } + }); + + // Create the view plugin for decorations + const dimPlugin = ViewPlugin.fromClass( + class implements PluginValue { + decorations: DecorationSet; + + constructor(view: EditorView) { + this.decorations = this.buildDecorations(view); + } + + update(update: ViewUpdate) { + // Update decorations if needed + if (update.docChanged || update.viewportChanged || pluginInstance.activeLine !== null) { + this.decorations = this.buildDecorations(update.view); + } + } + + buildDecorations(view: EditorView): DecorationSet { + // If no active line is set, return empty decorations + if (pluginInstance.activeLine === null) { + return Decoration.none; + } + + const builder = new RangeSetBuilder(); + const activeLine = pluginInstance.activeLine; + + // Add decorations to all lines except the active one + for (let i = 1; i <= view.state.doc.lines; i++) { + if (i !== activeLine) { + try { + const line = view.state.doc.line(i); + const decoration = Decoration.line({ + attributes: { class: "fh-dim" } + }); + builder.add(line.from, line.from, decoration); + } catch (e) { + console.error(`Error adding decoration to line ${i}:`, e); + } + } + } + + return builder.finish(); + } + + destroy() {} + }, + { + decorations: (instance) => instance.decorations + } + ); + + // Return both extensions + return [cursorTrackingField, dimPlugin]; + } private highlightSelection(): void { try { - console.log('VariantEditor: highlightSelection called'); - const view = this.app.workspace.getActiveViewOfType(MarkdownView); if (!view) { - console.log('VariantEditor: No active Markdown view'); return; } @@ -45,28 +149,18 @@ export default class VariantEditor extends Plugin { const selectedWord = selectedText.trim(); if (!selectedWord) { - console.log('VariantEditor: No text selected'); return; } - - const editorEl = view.contentEl; - if (!editorEl) { - console.log('VariantEditor: Could not find editor container'); - return; - } - - const lines = editorEl.querySelectorAll('.cm-line'); - console.log(`VariantEditor: Found ${lines.length} lines`); - - lines.forEach(line => { - if (!line.classList.contains('cm-active')) { - (line as HTMLElement).classList.add('fh-dim') - } - }); // Save cursor position for later text insertion const cursorPos = editor.getCursor(); + // Set active line for dimming + this.activeLine = cursorPos.line + 1; + + // Force editor refresh to apply decorations + this.app.workspace.updateOptions(); + // Open the text input modal after highlighting new TextInputModal(this.app, (inputText: string) => { if (inputText && inputText.trim()) { @@ -76,36 +170,24 @@ export default class VariantEditor extends Plugin { } }).open(); - console.log('VariantEditor: highlightSelection completed'); - } catch (e) { - console.error('VariantEditor: Error in highlightSelection:', e); + console.error('Error in highlightSelection:', e); } } private clearHighlight(): void { try { - console.log('VariantEditor: clearHighlight called'); + // Reset active line and previous cursor line + this.activeLine = null; + this.previousCursorLine = null; + // Use the hack to force a complete re-render const view = this.app.workspace.getActiveViewOfType(MarkdownView); - if (!view) { - console.log('VariantEditor: No active Markdown view in clearHighlight'); - return; + if (view) { + hackToRerender(view); } - - const editorEl = view.contentEl; - if (!editorEl) { - console.log('VariantEditor: Could not find editor container in clearHighlight'); - return; - } - - // Remove dim and active classes from all lines - const lines = editorEl.querySelectorAll('.cm-line'); - lines.forEach(line => (line as HTMLElement).classList.remove('fh-dim')); - console.log('VariantEditor: clearHighlight completed'); - } catch (e) { - console.error('VariantEditor: Error in clearHighlight:', e); + console.error('Error in clearHighlight:', e); } } } diff --git a/src/utils/editorUtils.ts b/src/utils/editorUtils.ts new file mode 100644 index 0000000..77dbb3c --- /dev/null +++ b/src/utils/editorUtils.ts @@ -0,0 +1,39 @@ +import { Editor, MarkdownView } from 'obsidian'; + +/** + * Forces a complete re-render of the editor using a zero-width space hack + * This is a workaround for cases where normal refresh methods don't work + * @param view The active MarkdownView + */ +export function hackToRerender(view: MarkdownView): void { + if (!view || !view.editor) { + return; + } + + try { + const editor = view.editor; + const cursorPos = editor.getCursor(); + + // Use setTimeout to avoid update-during-update errors + setTimeout(() => { + try { + // Insert a zero-width space character and then immediately remove it + // This forces a complete redraw without visible changes + const pos = { line: cursorPos.line, ch: cursorPos.ch }; + editor.replaceRange("\u200B", pos); // Insert zero-width space + editor.replaceRange("", pos, { line: pos.line, ch: pos.ch + 1 }); // Remove it + + // Also try to access the CM editor view directly if possible + const editorView = (view.editor as any).cm; + if (editorView) { + // Force a complete redraw + editorView.requestMeasure(); + } + } catch (err) { + console.error('Error in hackToRerender:', err); + } + }, 10); + } catch (e) { + console.error('Error setting up hackToRerender:', e); + } +} diff --git a/styles.css b/styles.css index d261e1e..447ef30 100644 --- a/styles.css +++ b/styles.css @@ -1,7 +1,8 @@ /* Variant Editor Plugin Styles */ /* Dimmed lines */ -.markdown-source-view .cm-line.fh-dim { +.markdown-source-view .cm-line.fh-dim, +.cm-line.fh-dim { opacity: 0.25; }