use codemirror edotor extenstions instead of dom manipulation

This commit is contained in:
kunalja 2025-06-07 01:07:59 -04:00
parent 7a9e5492b8
commit be09e60841
5 changed files with 213 additions and 48 deletions

View file

@ -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",

View file

@ -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':

View file

@ -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<Decoration>();
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);
}
}
}

39
src/utils/editorUtils.ts Normal file
View file

@ -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);
}
}

View file

@ -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;
}