/** * Editor layer types. Sync-backend-specific payloads live beside each editor * under `editors//`; only that editor reads `EditorBinding.payload`. */ import type { ComponentType } from 'react'; import type { DocHeading } from '../wikilinks/headings'; /** Sync/editor backend id. */ export type EditorKind = 'automerge'; /** * Editor rendering mode: * - `edit` — live preview (raw syntax reveals near the cursor for editing). * - `view` — reading mode (everything stays rendered; read-only). */ export type EditorMode = 'edit' | 'view'; export type EditorBinding = { kind: EditorKind; payload: unknown; }; /** * Where a tab was when you last left it. * * Switching tabs unmounts the editor (`DocumentSlotView` keys `DocumentPane` by * document id, and only the active tab's slot is rendered), so scroll position * and cursor would otherwise be lost every time. The workspace holds one of * these per tab and hands it back on the next mount. * * `scroll` is a CodeMirror scroll effect — opaque to everything but the editor * that produced it, and holding a live object, so this is in-memory only and is * deliberately never persisted. */ export type EditorViewSnapshot = { selection: { anchor: number; head: number }; scroll: unknown; /** * The `reveal.seq` already applied in this tab. Returning to the tab restores * the position above rather than re-running that navigation, so a heading * jump happens once and does not replay every time you come back. */ revealedSeq?: number; }; /** Where a `[[wikilink]]` target points, and whether that document exists yet. */ export type WikiLinkResolution = { /** * Provider-scoped document id (an absolute path today). For a resolved link * this is copied verbatim from the provider's listing, so it is byte-identical * to the ids the sidebar emits — which is what makes tab dedup (an exact * `providerId + entryId` string match) hit instead of opening a duplicate tab. * When `exists` is false it is the path we *would* create. */ entryId: string; /** Tab/title label — the filename, matching what the sidebar passes. */ title: string; exists: boolean; }; /** One candidate for the `[[` completion popup. */ export type WikiLinkCandidate = { /** Text inserted between the brackets: the bare name, or a path if ambiguous. */ label: string; /** Secondary text — the containing folder, relative to the vault root. */ detail: string; entryId: string; }; /** * A read-only view of the vault, supplied by the app so that pure * `EditorState` decoration builders and completion sources can resolve * wikilinks *synchronously*. * * A `null` view (rather than an empty one) means "the index has not loaded * yet" — links must then render neutral rather than unresolved, so a cold open * does not flash every link as broken. */ export type WikiLinkVaultView = { resolve(target: string): WikiLinkResolution | null; candidates(): readonly WikiLinkCandidate[]; }; /** * Headings of notes that are *not* open, for `[[Note#` completion. * * Explicitly asynchronous and deliberately separate from `WikiLinkVaultView`, * whose synchronous contract the decoration builders depend on. Keyed by * `entryId` rather than by target text, so it duplicates none of the resolution * logic in `wikiLinkIndex` — the caller resolves first, then asks here. */ export type WikiLinkHeadingSource = { load(entryId: string): Promise; }; /** A wikilink the user activated, already resolved against the vault. */ export type WikiLinkActivation = WikiLinkResolution & { /** `#heading` text without the `#`, or null. Carried but not yet acted on. */ anchor: string | null; /** The target text as typed, for diagnostics. */ rawTarget: string; }; /** Props passed from workspaces into every registered editor component. */ export type EditorProps = { binding: EditorBinding; className?: string; placeholder?: string; /** Rendering mode for this view; defaults to `edit`. */ mode?: EditorMode; /** * Called when a link to an internal (schemeless) target is activated — the raw * link string, for the workspace to resolve against the current document and * open. External links (with a URI scheme) are opened by the editor itself. */ onOpenLink?: (target: string) => void; /** * Vault knowledge for `[[wikilinks]]`: resolution (for rendering resolved vs * unresolved) and the candidate list (for completion). `null` while the * vault index is still loading. */ wikiLinkVault?: WikiLinkVaultView | null; /** Headings of other notes, for `[[Note#` completion. Null while unavailable. */ wikiLinkHeadings?: WikiLinkHeadingSource | null; /** * Called when a wikilink is activated, already resolved. Unlike `onOpenLink` * the editor cannot resolve this itself, so the app both resolves it (via * `wikiLinkVault`) and handles creating the note when it does not exist. */ onOpenWikiLink?: (activation: WikiLinkActivation) => void; /** * Position to restore on mount — where this tab was when it was last shown. * Absent for a document being opened for the first time. */ viewSnapshot?: EditorViewSnapshot | null; /** * Called as the editor tears down, with the position to restore next time. * The workspace stores it against the tab being left. */ onViewSnapshot?: (snapshot: EditorViewSnapshot) => void; /** * Scroll to and place the caret at a heading. `seq` is monotonic, so * requesting the *same* anchor again still fires; the editor ignores a seq it * has already applied, which is what stops a stale intent replaying when you * return to the tab. */ reveal?: { seq: number; anchor: string } | null; }; /** * A registered editor: UI for one sync method, keyed by `kind`. * Implementations live under `editors//` and are listed in `registry.ts`. */ export type Editor = { readonly kind: EditorKind; readonly Component: ComponentType; };