Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185// src/plugin/editor.tsimport 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<T>( 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 }) } }}