// src/plugin/editor.ts import type { EditorPosition, EditorRange, EditorSelection, EditorSelectionOrCaret, EditorTransaction, EditorChange, EditorCommandName, } from './types' /** * Abstract Editor class matching Obsidian's Editor API. * Implementations wrap the actual editor (CM6EditorAdapter). */ export abstract class Editor { /** * Returns this editor (for CodeMirror compatibility). */ getDoc(): this { return this } /** Refresh the editor view */ abstract refresh(): void /** Get full editor content */ abstract getValue(): string /** Set full editor content */ abstract setValue(content: string): void /** Get text at line (0-indexed) */ abstract getLine(line: number): string /** * Set text at line (0-indexed). * Default implementation uses replaceRange. */ setLine(n: number, text: string): void { const from: EditorPosition = { line: n, ch: 0 } const to: EditorPosition = { line: n, ch: this.getLine(n).length } this.replaceRange(text, from, to) } /** Get total number of lines */ abstract lineCount(): number /** Get index of last line */ abstract lastLine(): number /** Get selected text */ abstract getSelection(): string /** * Returns true if there is selected text. */ somethingSelected(): boolean { return this.getSelection().length > 0 } /** Get text in range */ abstract getRange(from: EditorPosition, to: EditorPosition): string /** Replace selected text */ abstract replaceSelection(replacement: string, origin?: string): void /** Replace text in range */ abstract replaceRange(replacement: string, from: EditorPosition, to?: EditorPosition, origin?: string): void /** Get cursor position */ abstract getCursor(side?: 'from' | 'to' | 'head' | 'anchor'): EditorPosition /** Get all selections */ abstract listSelections(): EditorSelection[] /** * Set cursor position. * Accepts either a position object or line + ch numbers. */ setCursor(pos: EditorPosition | number, ch?: number): void { if (typeof pos === 'number') { this.setSelection({ line: pos, ch: ch ?? 0 }) } else { this.setSelection(pos) } } /** Set selection */ abstract setSelection(anchor: EditorPosition, head?: EditorPosition): void /** * Set multiple selections. * Accepts EditorSelectionOrCaret[] for Obsidian compatibility. * If head is omitted on a range, it defaults to anchor (caret). */ abstract setSelections(ranges: EditorSelectionOrCaret[], main?: number): void /** Focus the editor */ abstract focus(): void /** Blur the editor */ abstract blur(): void /** Check if editor has focus */ abstract hasFocus(): boolean /** Get scroll information */ abstract getScrollInfo(): { top: number; left: number } /** Scroll to position */ abstract scrollTo(x?: number | null, y?: number | null): void /** Scroll range into view */ abstract scrollIntoView(range: EditorRange, center?: boolean): void /** Undo last change */ abstract undo(): void /** Redo last undone change */ abstract redo(): void /** Execute editor command */ abstract exec(command: EditorCommandName): void /** Apply transaction atomically */ abstract transaction(tx: EditorTransaction, origin?: string): void /** Get word at position */ abstract wordAt(pos: EditorPosition): EditorRange | null /** Convert position to offset */ abstract posToOffset(pos: EditorPosition): number /** Convert offset to position */ abstract offsetToPos(offset: number): EditorPosition /** * Process lines in the document with read and write callbacks. * * Iterates through all lines in the document. For each line: * 1. Calls `read(line, lineText)` to determine a value T or null * 2. Calls `write(line, lineText, value)` which may return an EditorChange * * All changes are collected and applied atomically via transaction. * * @param read - Called for each line. Returns a value T, or null to skip. * @param write - Called for each line with the read value. May return EditorChange to apply. * @param ignoreEmpty - If true, skip empty lines. * @public * @since 0.13.26 */ processLines( read: (line: number, lineText: string) => T | null, write: (line: number, lineText: string, value: T | null) => EditorChange | void, ignoreEmpty?: boolean ): void { const lineCount = this.lineCount() const changes: EditorChange[] = [] for (let line = 0; line < lineCount; line++) { const lineText = this.getLine(line) // Skip empty lines if ignoreEmpty is true if (ignoreEmpty && lineText.trim() === '') { continue } // Phase 1: read const value = read(line, lineText) // Phase 2: write const change = write(line, lineText, value) if (change) { changes.push(change) } } // Apply all changes atomically if (changes.length > 0) { this.transaction({ changes }) } } }