Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303// src/plugin/editor-suggest.ts/** * EditorSuggest framework. * * Implements Obsidian's EditorSuggest<T> public API for editor-anchored * autocomplete popovers (wikilinks, commands, mentions, etc). * * Inheritance chain matches Obsidian: * EditorSuggest<T> extends PopoverSuggest<T> * * Where PopoverSuggest<T> implements ISuggestOwner<T>, CloseableComponent * * `PopoverSuggest` is declared here (not in a separate file) so plugins * subclassing PopoverSuggest typecheck against our compat layer. Reams does * NOT yet implement non-editor popovers, so PopoverSuggest.open()/.close() * throw clear "not implemented" errors. */import type { Editor } from './editor'import type { TFile } from './files'import type { EditorPosition } from './types'import type { Instruction, CloseableComponent } from './obsidian-stubs'import { Scope } from './obsidian-stubs'
/** Result of EditorSuggest.onTrigger() — describes the trigger range. */export interface EditorSuggestTriggerInfo { /** Position where the trigger token starts (inclusive). */ start: EditorPosition /** Position of the cursor at trigger time (exclusive). */ end: EditorPosition /** The user's query string between trigger and cursor (excluding trigger chars). */ query: string}
/** * Context passed to getSuggestions / selectSuggestion. * @public * @since 0.12.17 */export interface EditorSuggestContext extends EditorSuggestTriggerInfo { /** @public */ editor: Editor /** @public */ file: TFile}
/** * Reams-internal extended context with AbortSignal support. * * This mirrors EditorSuggestContext but adds a `signal` property for * cancellation and allows `file: TFile | null` for unsaved docs. * * The dispatcher passes this to built-in suggesters and Reams-internal code. * External plugins receive the base type (they can still access `signal` at * runtime if present, but it's not part of the upstream contract). * * Contract: * - Suggesters performing fetch() or other cancellable async I/O SHOULD * pass `signal` through (e.g. `fetch(url, { signal: ctx.signal })`). * - Suggesters performing CPU-bound work in a tight loop SHOULD * periodically check `signal.aborted` and return [] early if true. * - Synchronous suggesters can safely ignore `signal`; the dispatcher * discards their results if the signal aborted before they ran. * * @internal */export interface ReamsEditorSuggestContext extends EditorSuggestTriggerInfo { /** Editor instance. */ editor: Editor /** * Source file, may be null for unsaved/untitled docs. * Upstream Obsidian requires TFile; Reams allows null. */ file: TFile | null /** * AbortSignal that fires when this trigger is superseded (newer keystroke, * dropdown closed, or editor unmounted). */ signal: AbortSignal}
/** * Input-side interface (parity with Obsidian's `ISuggestOwner`). The "owner" * is the suggester instance — render and select are owner methods called by * the popover/editor host. */export interface ISuggestOwner<T> { renderSuggestion(value: T, el: HTMLElement): void selectSuggestion(value: T, evt: MouseEvent | KeyboardEvent): void}
/** * Abstract base for any popover-style suggest UI (parity with Obsidian's * `PopoverSuggest<T>`). Obsidian uses this for non-editor popovers * (e.g. command palette inputs). * * Reams declares this as an abstract intermediate so the inheritance chain * `EditorSuggest<T> extends PopoverSuggest<T>` matches Obsidian. Plugins typed * `class X extends PopoverSuggest<T>` typecheck against our compat layer. * * Deliberate divergence: we do NOT implement `open()`/`close()` portal * scaffolding. Subclasses that try to use this base directly throw at * runtime — failure mode is loud, not silent. * * @public */export abstract class PopoverSuggest<T> implements ISuggestOwner<T>, CloseableComponent { /** @public */ app: import('./app').App
/** @public */ scope: Scope
/** @public */ constructor(app: import('./app').App, scope?: Scope) { this.app = app this.scope = scope ?? new Scope() }
abstract renderSuggestion(value: T, el: HTMLElement): void abstract selectSuggestion(value: T, evt: MouseEvent | KeyboardEvent): void
/** Stub: not implemented; subclass `EditorSuggest` instead for editor popovers. */ open(): void { throw new Error('PopoverSuggest.open() not implemented in Reams; subclass EditorSuggest instead') }
/** Stub: not implemented; subclass `EditorSuggest` instead for editor popovers. */ close(): void { throw new Error('PopoverSuggest.close() not implemented in Reams; subclass EditorSuggest instead') }}
/** * Base class for editor-anchored suggesters. Subclasses implement four methods: * - `onTrigger(cursor, editor, file)` — return triggerInfo or null on every keystroke. * The trigger MUST be single-line: `start.line` and `end.line` MUST equal * `cursor.line`, and `end` MUST equal `cursor`. Multi-line triggers are silently * rejected by the dispatcher's `pickWinningSuggesters`. * - `getSuggestions(context)` — return the list (may be async) * - `renderSuggestion(item, el)` — DOM rendering of one item * - `selectSuggestion(item, evt)` — handle item selection (mutate editor, etc.). * At call time, `this.context` holds the EditorSuggestContext that produced * the selection (matching Obsidian's documented contract). Use * `this.context.start`/`.end`/`.editor` to know the trigger range and the * editor to mutate; do NOT re-derive the range from the live cursor — * that would race against keystrokes between getSuggestions and select. */export abstract class EditorSuggest<T> extends PopoverSuggest<T> { /** * The active EditorSuggestContext, retained while suggestions are open and * cleared on dismissal. Mirrors * Obsidian's documented contract: subclasses read `this.context.start`, * `.end`, `.editor`, etc. inside `selectSuggestion` to mutate the editor. * * Stateful suggesters may reuse its start position on subsequent keystrokes. */ context: EditorSuggestContext | null = null
/** * Override this to use a different limit for suggestion items. * @since 0.12.17 (Obsidian API) * * Note: The Reams dispatcher does not yet enforce this limit. Subclasses * wanting to limit results should do so in getSuggestions() for now. */ limit: number = 100
constructor(app: import('./app').App) { super(app) }
/** * Set instructions to display at the bottom of the suggestion popover. * Instructions are hints to the user (e.g. "↵ to insert", "esc to dismiss"). * @since 0.13.0 (Obsidian API) * * Note: This is a stub for API compatibility. Reams does not yet render * instructions in the suggestion dropdown UI; calls are silently ignored. */ setInstructions(_instructions: Instruction[]): void { // Stub for API compatibility. Rendering not yet implemented. }
/** Called on every cursor/doc change. Return null for "no match". */ abstract onTrigger( cursor: EditorPosition, editor: Editor, file: TFile | null, ): EditorSuggestTriggerInfo | null
/** Return suggestions for the trigger context. Sync or async. */ abstract getSuggestions(context: EditorSuggestContext): T[] | Promise<T[]> // renderSuggestion / selectSuggestion are inherited as abstract from PopoverSuggest.}
/** * Attach to an `<input>` element or a `<div contentEditable>` to add type-ahead * support. * * Subclasses implement `getSuggestions(query)` and `renderSuggestion(item, el)`. * The `selectSuggestion` method is provided with a default that invokes the * `onSelect` callback; subclasses may override. * * This is a minimal implementation for Obsidian API compatibility. Reams does * NOT yet render a suggestion dropdown UI for AbstractInputSuggest, but its * lifecycle methods are safe no-ops so plugins can mount and unmount inputs. * Plugins wanting full autocomplete should use EditorSuggest instead. * * @public * @since 1.4.10 */export abstract class AbstractInputSuggest<T> extends PopoverSuggest<T> { /** * Limit to the number of elements rendered at once. Set to 0 to disable. Defaults to 100. * @public * @since 1.4.10 */ limit: number = 100
/** * The attached input or contenteditable element. * @internal */ #textInputEl: HTMLInputElement | HTMLDivElement
/** * Callback registered via onSelect(). * @internal */ // eslint-disable-next-line @typescript-eslint/no-explicit-any #onSelectCallback: ((value: T, evt: MouseEvent | KeyboardEvent) => any) | null = null
/** * Accepts an `<input>` text box or a contenteditable div. * @public */ constructor(app: import('./app').App, textInputEl: HTMLInputElement | HTMLDivElement) { super(app) this.#textInputEl = textInputEl }
/** No-op until Reams implements input-anchored suggestion popovers. */ override close(): void {}
/** * Sets the value into the input element. * @public * @since 1.4.10 */ setValue(value: string): void { if (this.#textInputEl instanceof HTMLInputElement) { this.#textInputEl.value = value } else { // contenteditable div this.#textInputEl.textContent = value } }
/** * Gets the value from the input element. * @public * @since 1.4.10 */ getValue(): string { if (this.#textInputEl instanceof HTMLInputElement) { return this.#textInputEl.value } else { // contenteditable div return this.#textInputEl.textContent ?? '' } }
/** * Return suggestions for the current query. May be async. * @public * @since 1.5.7 */ protected abstract getSuggestions(query: string): T[] | Promise<T[]>
/** * Handle suggestion selection. Default implementation invokes the onSelect * callback if registered. Subclasses may override for custom behavior. * @public * @since 1.6.6 */ selectSuggestion(value: T, evt: MouseEvent | KeyboardEvent): void { if (this.#onSelectCallback) { this.#onSelectCallback(value, evt) } }
/** * Registers a callback to handle when a suggestion is selected by the user. * @public * @since 1.4.10 */ // eslint-disable-next-line @typescript-eslint/no-explicit-any onSelect(callback: (value: T, evt: MouseEvent | KeyboardEvent) => any): this { this.#onSelectCallback = callback return this }}