Selection and captured intent
Use logical source coordinates and prevent asynchronous actions from writing to stale ranges.
Coordinates belong to source
MSFE uses UTF-16 offsets in CodeMirror's logical text and half-open [from, to) ranges. A preview has no independent coordinate system. An image or task control still owns a source range. Emoji may occupy two UTF-16 units; character counts and DOM offsets are not substitutes.
getMarkdown() may restore file line endings. Calculate edits against editor.state.doc, not byte offsets in serialized CRLF output. selection.main.from/to orders endpoints; anchor/head preserves direction.
import { EditorSelection } from '@quirekit/cm-state'
editor.setSelection(EditorSelection.single(10, 18))
editor.scrollTo(10)
editor.focus()
EditorSelection.cursor() returns one range, while setSelection() expects a complete selection. Use single() or create(). Define whether a command supports every selection or rejects multiple ranges; never silently modify only the first.
Short menus: capture an exact target
const target = editor.captureTarget()
// The host displays a menu; source may change before the user confirms.
const applied = editor.chain(target).insertText('approved').run()
if (!applied) console.warn(editor.lastCommandResult)
Targets include instance, document, revision and directed selection. Source changes may invalidate them. Retrying without the target after failure would redirect the action to a different selection.
Dialogs: map the original range
captureAnchor() maps across unrelated edits while verifying that the captured text remains unchanged. This complete helper suits asset or text selection dialogs. The host dialog must honor cancellation.
import type { MsfeEditor } from "@quirekit/msfe-core";
/** @param editor - Current document. @param choose - Host dialog, observing cancellation. @returns Whether a still-current range was updated. */
export async function replaceSelectionFromDialog(editor: MsfeEditor, choose: (signal: AbortSignal) => Promise<string | null>): Promise<boolean> {
const range = editor.state.selection.main;
const scope = editor.documentScope.fork();
const anchor = editor.captureAnchor({ from: range.from, to: range.to });
scope.own(() => anchor.release());
try {
const value = await choose(scope.signal);
if (scope.disposed || value === null) return false;
const target = anchor.target();
// Unrelated edits may move the interval; changes to its own text invalidate it.
return target ? editor.chain(target).insertText(value).run() : false;
} finally { scope.dispose(); }
}
Changing or deleting captured content, replacing the document or releasing the anchor invalidates its target. Document replacement invalidates old anchors even if the host reuses documentId. On false, ask the user to select again rather than forcing old offsets.
Mapping policy
By default, inclusiveStart and inclusiveEnd are false, excluding insertions exactly at either boundary. onDelete: 'drop' removes a fully deleted range; keep can retain metadata as a point. Comments, decorations and insertion intents may require different policies.
SourceRangeSet suits larger collections of range metadata. Prefer captureAnchor() for one asynchronous user intent instead of rebuilding mapping infrastructure.
Reveal, search and readonly
A native selection entering an ordinary replacement reveals source. A diagram's edit action therefore selects the appropriate source position. Atomic disclosure needs explicit state and interaction: 'atomic'; it must not permanently hide searchable content.
Readonly still permits selection and copying. Commands perform the final permission check: capturing an anchor never grants write permission. See Command API and Rendering API.