1// The atomic chip primitive: a StateField that recomputes replace-decorations from the document and
2// serves them as widget chips. Mentions build on it here; consumers build their own chip kinds on
3// the same seam (the acceptance test is that an app-specific paste decoration needs nothing else).
4import type { EditorState, Extension, Range, Transaction } from "@codemirror/state";
5import { StateField } from "@codemirror/state";
6import { Decoration, type DecorationSet, EditorView } from "@codemirror/view";
7
8export interface ChipSpec {
9 /** Recompute the chip decorations for a new state. Called on every doc change. */
10 chips(state: EditorState): Range<Decoration>[];
11 /** Also recompute on selection-only changes — for chips that reveal their source near the caret. */
12 onSelection?: boolean;
13 /** Consumer parser or configuration changes that affect the computed ranges. */
14 rebuild?: (transaction: Transaction) => boolean;
15 /** Register the set as atomic ranges: the caret skips a chip and Backspace deletes it whole. */
16 atomic?: boolean;
17}
18
19export function chipField(spec: ChipSpec): Extension {
20 const field = StateField.define<DecorationSet>({
21 create: (state) => Decoration.set(spec.chips(state), true),
22 update: (previous, transaction) =>
23 transaction.docChanged ||
24 (spec.onSelection && transaction.selection) ||
25 spec.rebuild?.(transaction)
26 ? Decoration.set(spec.chips(transaction.state), true)
27 : previous,
28 provide: (self) =>
29 spec.atomic
30 ? [
31 EditorView.decorations.from(self),
32 EditorView.atomicRanges.of((view) => view.state.field(self)),
33 ]
34 : EditorView.decorations.from(self),
35 });
36 return field;
37}