| 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). |
| 4 | import type { EditorState, Extension, Range, Transaction } from "@codemirror/state"; |
| 5 | import { StateField } from "@codemirror/state"; |
| 6 | import { Decoration, type DecorationSet, EditorView } from "@codemirror/view"; |
| 7 | |
| 8 | export 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 | |
| 19 | export 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 | } |