@dxos/ui-editor 0.0.0 → 0.8.4-main.03d5cd7b56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/index.mjs +8633 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/browser/types/index.mjs +33 -0
- package/dist/lib/browser/types/index.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +8635 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/lib/node-esm/types/index.mjs +35 -0
- package/dist/lib/node-esm/types/index.mjs.map +7 -0
- package/dist/types/src/defaults.d.ts +6 -0
- package/dist/types/src/defaults.d.ts.map +1 -0
- package/dist/types/src/extensions/annotations.d.ts +9 -0
- package/dist/types/src/extensions/annotations.d.ts.map +1 -0
- package/dist/types/src/extensions/auto-scroll.d.ts +18 -0
- package/dist/types/src/extensions/auto-scroll.d.ts.map +1 -0
- package/dist/types/src/extensions/autocomplete/autocomplete.d.ts +17 -0
- package/dist/types/src/extensions/autocomplete/autocomplete.d.ts.map +1 -0
- package/dist/types/src/extensions/autocomplete/index.d.ts +5 -0
- package/dist/types/src/extensions/autocomplete/index.d.ts.map +1 -0
- package/dist/types/src/extensions/autocomplete/match.d.ts +13 -0
- package/dist/types/src/extensions/autocomplete/match.d.ts.map +1 -0
- package/dist/types/src/extensions/autocomplete/placeholder.d.ts +23 -0
- package/dist/types/src/extensions/autocomplete/placeholder.d.ts.map +1 -0
- package/dist/types/src/extensions/autocomplete/typeahead.d.ts +10 -0
- package/dist/types/src/extensions/autocomplete/typeahead.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/automerge.d.ts +4 -0
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
- package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/cursor.d.ts +4 -0
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/defs.d.ts +17 -0
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/index.d.ts +2 -0
- package/dist/types/src/extensions/automerge/index.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/sync.d.ts +17 -0
- package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +6 -0
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/update-codemirror.d.ts +5 -0
- package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +1 -0
- package/dist/types/src/extensions/awareness/awareness-provider.d.ts +31 -0
- package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -0
- package/dist/types/src/extensions/awareness/awareness.d.ts +46 -0
- package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -0
- package/dist/types/src/extensions/awareness/index.d.ts +3 -0
- package/dist/types/src/extensions/awareness/index.d.ts.map +1 -0
- package/dist/types/src/extensions/blast.d.ts +25 -0
- package/dist/types/src/extensions/blast.d.ts.map +1 -0
- package/dist/types/src/extensions/blocks.d.ts +2 -0
- package/dist/types/src/extensions/blocks.d.ts.map +1 -0
- package/dist/types/src/extensions/bookmarks.d.ts +12 -0
- package/dist/types/src/extensions/bookmarks.d.ts.map +1 -0
- package/dist/types/src/extensions/comments.d.ts +90 -0
- package/dist/types/src/extensions/comments.d.ts.map +1 -0
- package/dist/types/src/extensions/debug.d.ts +3 -0
- package/dist/types/src/extensions/debug.d.ts.map +1 -0
- package/dist/types/src/extensions/dnd.d.ts +9 -0
- package/dist/types/src/extensions/dnd.d.ts.map +1 -0
- package/dist/types/src/extensions/factories.d.ts +88 -0
- package/dist/types/src/extensions/factories.d.ts.map +1 -0
- package/dist/types/src/extensions/factories.test.d.ts +2 -0
- package/dist/types/src/extensions/factories.test.d.ts.map +1 -0
- package/dist/types/src/extensions/focus.d.ts +7 -0
- package/dist/types/src/extensions/focus.d.ts.map +1 -0
- package/dist/types/src/extensions/folding.d.ts +6 -0
- package/dist/types/src/extensions/folding.d.ts.map +1 -0
- package/dist/types/src/extensions/hashtag.d.ts +3 -0
- package/dist/types/src/extensions/hashtag.d.ts.map +1 -0
- package/dist/types/src/extensions/index.d.ts +32 -0
- package/dist/types/src/extensions/index.d.ts.map +1 -0
- package/dist/types/src/extensions/json.d.ts +7 -0
- package/dist/types/src/extensions/json.d.ts.map +1 -0
- package/dist/types/src/extensions/listener.d.ts +13 -0
- package/dist/types/src/extensions/listener.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/action.d.ts +12 -0
- package/dist/types/src/extensions/markdown/action.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/bundle.d.ts +25 -0
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/changes.d.ts +10 -0
- package/dist/types/src/extensions/markdown/changes.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/changes.test.d.ts +2 -0
- package/dist/types/src/extensions/markdown/changes.test.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/debug.d.ts +11 -0
- package/dist/types/src/extensions/markdown/debug.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/decorate.d.ts +25 -0
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts +63 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/formatting.test.d.ts +3 -0
- package/dist/types/src/extensions/markdown/formatting.test.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/highlight.d.ts +37 -0
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/image.d.ts +7 -0
- package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/index.d.ts +10 -0
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/link.d.ts +7 -0
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/parser.test.d.ts +2 -0
- package/dist/types/src/extensions/markdown/parser.test.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/styles.d.ts +4 -0
- package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/table.d.ts +8 -0
- package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
- package/dist/types/src/extensions/mention.d.ts +7 -0
- package/dist/types/src/extensions/mention.d.ts.map +1 -0
- package/dist/types/src/extensions/modal.d.ts +7 -0
- package/dist/types/src/extensions/modal.d.ts.map +1 -0
- package/dist/types/src/extensions/modes.d.ts +10 -0
- package/dist/types/src/extensions/modes.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/commands.d.ts +10 -0
- package/dist/types/src/extensions/outliner/commands.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/editor.d.ts +5 -0
- package/dist/types/src/extensions/outliner/editor.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/editor.test.d.ts +2 -0
- package/dist/types/src/extensions/outliner/editor.test.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/index.d.ts +4 -0
- package/dist/types/src/extensions/outliner/index.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/menu.d.ts +8 -0
- package/dist/types/src/extensions/outliner/menu.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/outliner.d.ts +11 -0
- package/dist/types/src/extensions/outliner/outliner.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/outliner.test.d.ts +2 -0
- package/dist/types/src/extensions/outliner/outliner.test.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/selection.d.ts +12 -0
- package/dist/types/src/extensions/outliner/selection.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/tree.d.ts +79 -0
- package/dist/types/src/extensions/outliner/tree.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/tree.test.d.ts +2 -0
- package/dist/types/src/extensions/outliner/tree.test.d.ts.map +1 -0
- package/dist/types/src/extensions/preview/index.d.ts +2 -0
- package/dist/types/src/extensions/preview/index.d.ts.map +1 -0
- package/dist/types/src/extensions/preview/preview.d.ts +34 -0
- package/dist/types/src/extensions/preview/preview.d.ts.map +1 -0
- package/dist/types/src/extensions/replacer.d.ts +21 -0
- package/dist/types/src/extensions/replacer.d.ts.map +1 -0
- package/dist/types/src/extensions/replacer.test.d.ts +2 -0
- package/dist/types/src/extensions/replacer.test.d.ts.map +1 -0
- package/dist/types/src/extensions/scroll-past-end.d.ts +3 -0
- package/dist/types/src/extensions/scroll-past-end.d.ts.map +1 -0
- package/dist/types/src/extensions/scroller.d.ts +68 -0
- package/dist/types/src/extensions/scroller.d.ts.map +1 -0
- package/dist/types/src/extensions/selection.d.ts +24 -0
- package/dist/types/src/extensions/selection.d.ts.map +1 -0
- package/dist/types/src/extensions/snippets.d.ts +10 -0
- package/dist/types/src/extensions/snippets.d.ts.map +1 -0
- package/dist/types/src/extensions/state.d.ts +2 -0
- package/dist/types/src/extensions/state.d.ts.map +1 -0
- package/dist/types/src/extensions/submit.d.ts +10 -0
- package/dist/types/src/extensions/submit.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/extended-markdown.d.ts +10 -0
- package/dist/types/src/extensions/tags/extended-markdown.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/extended-markdown.test.d.ts +2 -0
- package/dist/types/src/extensions/tags/extended-markdown.test.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/fader.d.ts +12 -0
- package/dist/types/src/extensions/tags/fader.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/index.d.ts +7 -0
- package/dist/types/src/extensions/tags/index.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/typewriter.d.ts +43 -0
- package/dist/types/src/extensions/tags/typewriter.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/typewriter.test.d.ts +2 -0
- package/dist/types/src/extensions/tags/typewriter.test.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/xml-block-decoration.d.ts +31 -0
- package/dist/types/src/extensions/tags/xml-block-decoration.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/xml-formatting.d.ts +24 -0
- package/dist/types/src/extensions/tags/xml-formatting.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/xml-tags.d.ts +117 -0
- package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/xml-util.d.ts +10 -0
- package/dist/types/src/extensions/tags/xml-util.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/xml-util.test.d.ts +2 -0
- package/dist/types/src/extensions/tags/xml-util.test.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +8 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/styles/index.d.ts +2 -0
- package/dist/types/src/styles/index.d.ts.map +1 -0
- package/dist/types/src/styles/theme.d.ts +58 -0
- package/dist/types/src/styles/theme.d.ts.map +1 -0
- package/dist/types/src/types/index.d.ts +2 -0
- package/dist/types/src/types/index.d.ts.map +1 -0
- package/dist/types/src/types/types.d.ts +21 -0
- package/dist/types/src/types/types.d.ts.map +1 -0
- package/dist/types/src/util/cursor.d.ts +31 -0
- package/dist/types/src/util/cursor.d.ts.map +1 -0
- package/dist/types/src/util/debug.d.ts +17 -0
- package/dist/types/src/util/debug.d.ts.map +1 -0
- package/dist/types/src/util/decorations.d.ts +4 -0
- package/dist/types/src/util/decorations.d.ts.map +1 -0
- package/dist/types/src/util/dom.d.ts +10 -0
- package/dist/types/src/util/dom.d.ts.map +1 -0
- package/dist/types/src/util/facet.d.ts +3 -0
- package/dist/types/src/util/facet.d.ts.map +1 -0
- package/dist/types/src/util/index.d.ts +7 -0
- package/dist/types/src/util/index.d.ts.map +1 -0
- package/dist/types/src/util/util.d.ts +8 -0
- package/dist/types/src/util/util.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +42 -43
- package/src/defaults.ts +33 -20
- package/src/extensions/annotations.ts +1 -1
- package/src/extensions/auto-scroll.ts +234 -0
- package/src/extensions/autocomplete/placeholder.ts +37 -18
- package/src/extensions/automerge/automerge.test.tsx +37 -11
- package/src/extensions/automerge/automerge.ts +5 -7
- package/src/extensions/blocks.ts +5 -5
- package/src/extensions/comments.ts +5 -6
- package/src/extensions/dnd.ts +2 -2
- package/src/extensions/factories.test.ts +88 -0
- package/src/extensions/factories.ts +32 -15
- package/src/extensions/folding.ts +5 -22
- package/src/extensions/index.ts +4 -3
- package/src/extensions/markdown/action.ts +0 -1
- package/src/extensions/markdown/bundle.ts +23 -9
- package/src/extensions/markdown/decorate.ts +15 -12
- package/src/extensions/markdown/formatting.ts +5 -10
- package/src/extensions/markdown/highlight.ts +15 -7
- package/src/extensions/markdown/link.ts +27 -33
- package/src/extensions/markdown/parser.test.ts +0 -1
- package/src/extensions/markdown/styles.ts +42 -9
- package/src/extensions/markdown/table.ts +24 -2
- package/src/extensions/outliner/outliner.test.ts +0 -1
- package/src/extensions/outliner/outliner.ts +3 -4
- package/src/extensions/outliner/tree.test.ts +0 -1
- package/src/extensions/preview/preview.ts +62 -15
- package/src/extensions/scroll-past-end.ts +32 -0
- package/src/extensions/scroller.ts +256 -0
- package/src/extensions/selection.ts +1 -1
- package/src/extensions/snippets.ts +67 -0
- package/src/extensions/tags/extended-markdown.test.ts +120 -2
- package/src/extensions/tags/extended-markdown.ts +80 -1
- package/src/extensions/tags/fader.ts +195 -0
- package/src/extensions/tags/index.ts +4 -1
- package/src/extensions/tags/testing/text.md +36 -0
- package/src/extensions/tags/testing/text.txt +35 -0
- package/src/extensions/tags/typewriter.test.ts +65 -0
- package/src/extensions/tags/typewriter.ts +594 -0
- package/src/extensions/tags/xml-block-decoration.ts +123 -0
- package/src/extensions/tags/xml-formatting.ts +125 -0
- package/src/extensions/tags/xml-tags.ts +186 -35
- package/src/extensions/tags/xml-util.test.ts +199 -24
- package/src/extensions/tags/xml-util.ts +62 -5
- package/src/index.ts +0 -1
- package/src/styles/index.ts +0 -2
- package/src/styles/theme.ts +124 -33
- package/src/types/types.ts +10 -2
- package/src/typings.d.ts +8 -0
- package/src/util/cursor.ts +1 -2
- package/src/extensions/autoscroll.ts +0 -165
- package/src/extensions/scrolling.ts +0 -189
- package/src/extensions/tags/streamer.ts +0 -243
- package/src/extensions/typewriter.ts +0 -68
- package/src/styles/markdown.ts +0 -26
- package/src/styles/tokens.ts +0 -17
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { xmlLanguage } from '@codemirror/lang-xml';
|
|
6
|
+
import { type Extension, type Range } from '@codemirror/state';
|
|
7
|
+
import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
|
|
8
|
+
|
|
9
|
+
export type XmlBlockDecorationOptions = {
|
|
10
|
+
/**
|
|
11
|
+
* Tag name to match (e.g. `'prompt'`).
|
|
12
|
+
*/
|
|
13
|
+
tag: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Class added via `Decoration.line` to each line that intersects the element's content
|
|
17
|
+
* range. Use to style the bubble container (e.g. flex alignment, vertical margin).
|
|
18
|
+
*/
|
|
19
|
+
lineClass?: string;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Class added via `Decoration.mark` covering the inner content (between the open and
|
|
23
|
+
* close tags). Use to style the bubble surface (background, padding, rounding).
|
|
24
|
+
*/
|
|
25
|
+
contentClass?: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* When true, the open and close tag delimiters are hidden via `Decoration.replace`
|
|
29
|
+
* with no widget, so the rendered text is the inner content only.
|
|
30
|
+
*/
|
|
31
|
+
hideTags?: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Walks the doc with the Lezer XML parser and decorates `<tag>…</tag>` elements without
|
|
36
|
+
* replacing them with a widget — the source text remains in the document and can still
|
|
37
|
+
* be matched by other extensions (e.g. `xmlFormatting`). Use this for "bubble"-style
|
|
38
|
+
* styling of XML blocks (chat prompts, callouts, etc.) where the inner content should
|
|
39
|
+
* stay editable/copyable rather than living inside a CodeMirror widget.
|
|
40
|
+
*/
|
|
41
|
+
export const xmlBlockDecoration = ({
|
|
42
|
+
tag,
|
|
43
|
+
lineClass,
|
|
44
|
+
contentClass,
|
|
45
|
+
hideTags,
|
|
46
|
+
}: XmlBlockDecorationOptions): Extension => {
|
|
47
|
+
const lineDecoration = lineClass ? Decoration.line({ class: lineClass }) : undefined;
|
|
48
|
+
const contentDecoration = contentClass ? Decoration.mark({ class: contentClass }) : undefined;
|
|
49
|
+
const hideDecoration = hideTags ? Decoration.replace({}) : undefined;
|
|
50
|
+
|
|
51
|
+
const buildDecorations = (view: EditorView): DecorationSet => {
|
|
52
|
+
const text = view.state.sliceDoc(0, view.state.doc.length);
|
|
53
|
+
if (!text.includes(`<${tag}`)) {
|
|
54
|
+
return Decoration.none;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const tree = xmlLanguage.parser.parse(text);
|
|
58
|
+
const ranges: Range<Decoration>[] = [];
|
|
59
|
+
tree.iterate({
|
|
60
|
+
enter: (node) => {
|
|
61
|
+
if (node.type.name !== 'Element') {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const openTag = node.node.getChild('OpenTag');
|
|
65
|
+
const closeTag = node.node.getChild('CloseTag') ?? node.node.getChild('MismatchedCloseTag');
|
|
66
|
+
const tagNameNode = openTag?.getChild('TagName');
|
|
67
|
+
if (!openTag || !tagNameNode) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (text.slice(tagNameNode.from, tagNameNode.to) !== tag) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const contentFrom = openTag.to;
|
|
75
|
+
const contentTo = closeTag?.from ?? node.node.to;
|
|
76
|
+
|
|
77
|
+
if (hideDecoration) {
|
|
78
|
+
ranges.push(hideDecoration.range(openTag.from, openTag.to));
|
|
79
|
+
if (closeTag) {
|
|
80
|
+
ranges.push(hideDecoration.range(closeTag.from, closeTag.to));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (contentDecoration && contentFrom < contentTo) {
|
|
85
|
+
ranges.push(contentDecoration.range(contentFrom, contentTo));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (lineDecoration && contentFrom <= view.state.doc.length) {
|
|
89
|
+
// Apply line decoration to every line that intersects the content range.
|
|
90
|
+
let pos = contentFrom;
|
|
91
|
+
while (pos <= contentTo && pos <= view.state.doc.length) {
|
|
92
|
+
const line = view.state.doc.lineAt(pos);
|
|
93
|
+
ranges.push(lineDecoration.range(line.from));
|
|
94
|
+
if (line.to >= contentTo) {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
pos = line.to + 1;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
return Decoration.set(ranges, true);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return ViewPlugin.fromClass(
|
|
106
|
+
class {
|
|
107
|
+
decorations: DecorationSet;
|
|
108
|
+
|
|
109
|
+
constructor(view: EditorView) {
|
|
110
|
+
this.decorations = buildDecorations(view);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
update(update: ViewUpdate) {
|
|
114
|
+
if (update.docChanged) {
|
|
115
|
+
this.decorations = buildDecorations(update.view);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
decorations: (instance) => instance.decorations,
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { xmlLanguage } from '@codemirror/lang-xml';
|
|
6
|
+
import { type Extension, type Range } from '@codemirror/state';
|
|
7
|
+
import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Lezer XML node names that represent the angle-bracket delimited portions of an element.
|
|
11
|
+
*/
|
|
12
|
+
const XML_TAG_NODES = new Set(['OpenTag', 'CloseTag', 'SelfClosingTag', 'MismatchedCloseTag']);
|
|
13
|
+
|
|
14
|
+
const xmlElementMark = Decoration.mark({ class: 'cm-xml-element' });
|
|
15
|
+
const xmlTagMark = Decoration.mark({ class: 'cm-xml-tag' });
|
|
16
|
+
const xmlContentMark = Decoration.mark({ class: 'cm-xml-content' });
|
|
17
|
+
|
|
18
|
+
export type XmlFormattingOptions = {
|
|
19
|
+
/**
|
|
20
|
+
* Tag names whose elements should NOT receive xmlFormatting decorations. Use to
|
|
21
|
+
* opt-out tags rendered by another extension (e.g. `xmlBlockDecoration` for `<prompt>`)
|
|
22
|
+
* so the two don't double-wrap the same content with stacked styling.
|
|
23
|
+
*
|
|
24
|
+
* Skipping is recursive: descendants of a skipped element are also untouched, so a
|
|
25
|
+
* `<foo>` inside a skipped `<prompt>` still appears literally without xmlFormatting
|
|
26
|
+
* styling.
|
|
27
|
+
*/
|
|
28
|
+
skip?: string[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Mark decoration extension that highlights XML tag delimiters
|
|
33
|
+
* (e.g., `<tag>`, `</tag>`, `<tag attr="x"/>`) with the `cm-xml-tag` class.
|
|
34
|
+
*
|
|
35
|
+
* Uses `@codemirror/lang-xml`'s Lezer parser standalone — only to compute
|
|
36
|
+
* decoration ranges — without changing the editor's primary language. This
|
|
37
|
+
* keeps the document plain text while still handling nesting and attributes
|
|
38
|
+
* correctly.
|
|
39
|
+
*/
|
|
40
|
+
export const xmlFormatting = ({ skip }: XmlFormattingOptions = {}): Extension => {
|
|
41
|
+
const skipSet = skip && skip.length > 0 ? new Set(skip) : undefined;
|
|
42
|
+
|
|
43
|
+
const buildDecorations = (view: EditorView): DecorationSet => {
|
|
44
|
+
const text = view.state.sliceDoc(0, view.state.doc.length);
|
|
45
|
+
if (!text.includes('<')) {
|
|
46
|
+
return Decoration.none;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const tagNameAt = (node: { from: number; to: number }) => text.slice(node.from, node.to);
|
|
50
|
+
|
|
51
|
+
const tree = xmlLanguage.parser.parse(text);
|
|
52
|
+
const ranges: Range<Decoration>[] = [];
|
|
53
|
+
tree.iterate({
|
|
54
|
+
enter: (node) => {
|
|
55
|
+
const name = node.type.name;
|
|
56
|
+
if (name === 'SelfClosingTag' && node.from < node.to) {
|
|
57
|
+
if (skipSet) {
|
|
58
|
+
const tagNameNode = node.node.getChild('TagName');
|
|
59
|
+
if (tagNameNode && skipSet.has(tagNameAt(tagNameNode))) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Self-closing tag is its own outer block and tag delimiter.
|
|
64
|
+
ranges.push(xmlElementMark.range(node.from, node.to));
|
|
65
|
+
ranges.push(xmlTagMark.range(node.from, node.to));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (XML_TAG_NODES.has(name) && node.from < node.to) {
|
|
69
|
+
ranges.push(xmlTagMark.range(node.from, node.to));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (name === 'Element' && node.from < node.to) {
|
|
73
|
+
const openTag = node.node.getChild('OpenTag');
|
|
74
|
+
if (openTag && skipSet) {
|
|
75
|
+
const tagNameNode = openTag.getChild('TagName');
|
|
76
|
+
if (tagNameNode && skipSet.has(tagNameAt(tagNameNode))) {
|
|
77
|
+
// Skip this element AND its descendants — another extension owns rendering.
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const closeTag = node.node.getChild('CloseTag') ?? node.node.getChild('MismatchedCloseTag');
|
|
82
|
+
ranges.push(xmlElementMark.range(node.from, node.to));
|
|
83
|
+
if (openTag && closeTag && openTag.to < closeTag.from) {
|
|
84
|
+
ranges.push(xmlContentMark.range(openTag.to, closeTag.from));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
return Decoration.set(ranges, true);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return [
|
|
93
|
+
ViewPlugin.fromClass(
|
|
94
|
+
class {
|
|
95
|
+
decorations: DecorationSet;
|
|
96
|
+
|
|
97
|
+
constructor(view: EditorView) {
|
|
98
|
+
this.decorations = buildDecorations(view);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
update(update: ViewUpdate) {
|
|
102
|
+
if (update.docChanged) {
|
|
103
|
+
this.decorations = buildDecorations(update.view);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
decorations: (instance) => instance.decorations,
|
|
109
|
+
},
|
|
110
|
+
),
|
|
111
|
+
|
|
112
|
+
EditorView.baseTheme({
|
|
113
|
+
'.cm-xml-element': {
|
|
114
|
+
backgroundColor: 'var(--color-active-surface)',
|
|
115
|
+
borderRadius: '0.25rem',
|
|
116
|
+
padding: '0.25rem',
|
|
117
|
+
},
|
|
118
|
+
'.cm-xml-tag': {
|
|
119
|
+
color: 'var(--color-blue-500)',
|
|
120
|
+
fontFamily: 'var(--font-mono)',
|
|
121
|
+
},
|
|
122
|
+
'.cm-xml-content': {},
|
|
123
|
+
}),
|
|
124
|
+
];
|
|
125
|
+
};
|
|
@@ -13,16 +13,15 @@ import {
|
|
|
13
13
|
WidgetType,
|
|
14
14
|
keymap,
|
|
15
15
|
} from '@codemirror/view';
|
|
16
|
-
// TODO(burdon): Factor out agnostic types (React/solid).
|
|
17
16
|
import { type FunctionComponent } from 'react';
|
|
18
17
|
|
|
19
18
|
import { invariant } from '@dxos/invariant';
|
|
20
19
|
import { log } from '@dxos/log';
|
|
20
|
+
import { Domino } from '@dxos/ui';
|
|
21
21
|
|
|
22
22
|
import { type Range } from '../../types';
|
|
23
23
|
import { decorationSetToArray } from '../../util';
|
|
24
|
-
import {
|
|
25
|
-
|
|
24
|
+
import { scrollerLineEffect } from '../scroller';
|
|
26
25
|
import { nodeToJson } from './xml-util';
|
|
27
26
|
|
|
28
27
|
/**
|
|
@@ -54,8 +53,9 @@ export type XmlEventHandler<TEvent = any> = (event: TEvent) => void;
|
|
|
54
53
|
*/
|
|
55
54
|
export type XmlWidgetProps<TProps = any, TContext = any> = TProps & {
|
|
56
55
|
_tag: string;
|
|
56
|
+
range: { from: number; to: number };
|
|
57
|
+
children?: any[];
|
|
57
58
|
context?: TContext;
|
|
58
|
-
range?: { from: number; to: number };
|
|
59
59
|
view?: EditorView;
|
|
60
60
|
onEvent?: XmlEventHandler;
|
|
61
61
|
};
|
|
@@ -63,19 +63,40 @@ export type XmlWidgetProps<TProps = any, TContext = any> = TProps & {
|
|
|
63
63
|
/**
|
|
64
64
|
* Factory for creating widgets.
|
|
65
65
|
*/
|
|
66
|
-
export type XmlWidgetFactory = (props: XmlWidgetProps
|
|
66
|
+
export type XmlWidgetFactory = (props: XmlWidgetProps) => WidgetType | null;
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* Widget registry definition.
|
|
70
|
+
* NOTE: Widgets should NOT use top/bottom margins (it causes unstable measurements while scrolling which leads to jumps).
|
|
71
|
+
* If required, use encapsulated divs with padding instead.
|
|
70
72
|
*/
|
|
71
73
|
export type XmlWidgetDef = {
|
|
72
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Block widget.
|
|
76
|
+
*/
|
|
73
77
|
block?: boolean;
|
|
74
78
|
|
|
75
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* When true, the opening tag is flushed immediately and inner content streams character-by-character.
|
|
81
|
+
*/
|
|
82
|
+
streaming?: boolean;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Debug only.
|
|
86
|
+
*/
|
|
87
|
+
debug?: boolean;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Native widget (rendered inline).
|
|
91
|
+
*/
|
|
76
92
|
factory?: XmlWidgetFactory;
|
|
77
93
|
|
|
78
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* React/Solid widget (rendered in portals outside of the editor).
|
|
96
|
+
* Prefer an `id="..."` attribute on the tag so `updateWidget` can target the instance; if omitted,
|
|
97
|
+
* an id is derived from the tag’s document range (non-streaming) or opening position (streaming).
|
|
98
|
+
* Streaming tags use `cm-xml-<from>` so the same portal id is kept when the closing tag arrives.
|
|
99
|
+
*/
|
|
79
100
|
Component?: FunctionComponent<XmlWidgetProps>;
|
|
80
101
|
};
|
|
81
102
|
|
|
@@ -86,6 +107,13 @@ export const getXmlTextChild = (children: any[]): string | null => {
|
|
|
86
107
|
return typeof child === 'string' ? child : null;
|
|
87
108
|
};
|
|
88
109
|
|
|
110
|
+
/** Stable id for portaled React/Solid widgets; explicit `id` on the tag wins for `updateWidget`. */
|
|
111
|
+
const xmlWidgetId = (explicit: unknown, fallback: string): string =>
|
|
112
|
+
typeof explicit === 'string' && explicit.length > 0 ? explicit : fallback;
|
|
113
|
+
|
|
114
|
+
/** Escapes a string for safe embedding in RegExp source (tag names from the registry). */
|
|
115
|
+
const escapeRegExpSource = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
116
|
+
|
|
89
117
|
/**
|
|
90
118
|
* Update context.
|
|
91
119
|
*/
|
|
@@ -103,6 +131,8 @@ export const xmlTagUpdateEffect = StateEffect.define<{ id: string; value: any }>
|
|
|
103
131
|
|
|
104
132
|
type WidgetDecorationSet = {
|
|
105
133
|
from: number;
|
|
134
|
+
/** Start position of an active unclosed streaming tag (for rebuild range). */
|
|
135
|
+
streamingFrom?: number;
|
|
106
136
|
decorations: DecorationSet;
|
|
107
137
|
};
|
|
108
138
|
|
|
@@ -164,11 +194,11 @@ export type XmlTagsOptions = {
|
|
|
164
194
|
/** Tag registry. */
|
|
165
195
|
registry?: XmlWidgetRegistry;
|
|
166
196
|
|
|
197
|
+
/** Tags to bookmark for navigation. */
|
|
198
|
+
bookmarks?: string[];
|
|
199
|
+
|
|
167
200
|
/** Called when widgets are mounted or unmounted. */
|
|
168
201
|
setWidgets?: (widgets: XmlWidgetState[]) => void;
|
|
169
|
-
|
|
170
|
-
/** Tags to bookmark. */
|
|
171
|
-
bookmarks?: string[];
|
|
172
202
|
};
|
|
173
203
|
|
|
174
204
|
/**
|
|
@@ -184,6 +214,7 @@ export type XmlTagsOptions = {
|
|
|
184
214
|
export const xmlTags = ({ registry, setWidgets, bookmarks }: XmlTagsOptions = {}): Extension => {
|
|
185
215
|
const notifier = createWidgetMap(setWidgets);
|
|
186
216
|
const widgetDecorationsField = createWidgetDecorationsField(registry, notifier);
|
|
217
|
+
|
|
187
218
|
return [
|
|
188
219
|
widgetContextStateField,
|
|
189
220
|
widgetStateMapStateField,
|
|
@@ -268,7 +299,7 @@ const createNavigationEffectPlugin = (
|
|
|
268
299
|
const line = view.state.doc.lineAt(widget?.from ?? 0);
|
|
269
300
|
view.dispatch({
|
|
270
301
|
selection: { anchor: line.from, head: line.from },
|
|
271
|
-
effects:
|
|
302
|
+
effects: scrollerLineEffect.of({ line: line.number - 1, offset: -16 }),
|
|
272
303
|
});
|
|
273
304
|
|
|
274
305
|
continue;
|
|
@@ -294,13 +325,13 @@ const createNavigationEffectPlugin = (
|
|
|
294
325
|
const line = view.state.doc.lineAt(widget?.from);
|
|
295
326
|
view.dispatch({
|
|
296
327
|
selection: { anchor: line.to, head: line.to },
|
|
297
|
-
effects:
|
|
328
|
+
effects: scrollerLineEffect.of({ line: line.number - 1, offset: -16 }),
|
|
298
329
|
});
|
|
299
330
|
} else {
|
|
300
331
|
const line = view.state.doc.lineAt(view.state.doc.length);
|
|
301
332
|
view.dispatch({
|
|
302
333
|
selection: { anchor: line.to, head: line.to },
|
|
303
|
-
effects:
|
|
334
|
+
effects: scrollerLineEffect.of({ line: line.number - 1, position: 'end' }),
|
|
304
335
|
});
|
|
305
336
|
}
|
|
306
337
|
|
|
@@ -355,10 +386,13 @@ const createWidgetDecorationsField = (registry: XmlWidgetRegistry = {}, notifier
|
|
|
355
386
|
create: (state) => {
|
|
356
387
|
return buildDecorations(state, { from: 0, to: state.doc.length }, registry, notifier);
|
|
357
388
|
},
|
|
358
|
-
update: ({ from, decorations }, tr) => {
|
|
389
|
+
update: ({ from, streamingFrom, decorations }, tr) => {
|
|
359
390
|
// Check for reset effect.
|
|
360
391
|
for (const effect of tr.effects) {
|
|
361
392
|
if (effect.is(xmlTagResetEffect)) {
|
|
393
|
+
if (tr.docChanged) {
|
|
394
|
+
return buildDecorations(tr.state, { from: 0, to: tr.state.doc.length }, registry, notifier);
|
|
395
|
+
}
|
|
362
396
|
return { from: 0, decorations: Decoration.none };
|
|
363
397
|
}
|
|
364
398
|
}
|
|
@@ -372,16 +406,22 @@ const createWidgetDecorationsField = (registry: XmlWidgetRegistry = {}, notifier
|
|
|
372
406
|
// Full rebuild from start.
|
|
373
407
|
return buildDecorations(state, { from: 0, to: state.doc.length }, registry, notifier);
|
|
374
408
|
} else {
|
|
375
|
-
//
|
|
376
|
-
const
|
|
409
|
+
// Rebuild from the streaming tag start (if active) so the tree walk can detect completion.
|
|
410
|
+
const rebuildFrom = streamingFrom ?? from;
|
|
411
|
+
const result = buildDecorations(state, { from: rebuildFrom, to: state.doc.length }, registry, notifier);
|
|
377
412
|
return {
|
|
378
413
|
from: result.from,
|
|
379
|
-
|
|
414
|
+
streamingFrom: result.streamingFrom,
|
|
415
|
+
decorations: decorations.update({
|
|
416
|
+
// Remove old streaming decorations — they are rebuilt each tick.
|
|
417
|
+
filter: (_f, _t, deco) => !deco.spec.streaming,
|
|
418
|
+
add: decorationSetToArray(result.decorations),
|
|
419
|
+
}),
|
|
380
420
|
};
|
|
381
421
|
}
|
|
382
422
|
}
|
|
383
423
|
|
|
384
|
-
return { from, decorations };
|
|
424
|
+
return { from, streamingFrom, decorations };
|
|
385
425
|
},
|
|
386
426
|
provide: (field) => [
|
|
387
427
|
EditorView.decorations.from(field, (v) => v.decorations),
|
|
@@ -391,6 +431,7 @@ const createWidgetDecorationsField = (registry: XmlWidgetRegistry = {}, notifier
|
|
|
391
431
|
|
|
392
432
|
/**
|
|
393
433
|
* Creates widget decorations for XML tags in the document using the syntax tree.
|
|
434
|
+
* After the tree walk, scans for unclosed streaming tags and creates provisional decorations.
|
|
394
435
|
*/
|
|
395
436
|
const buildDecorations = (
|
|
396
437
|
state: EditorState,
|
|
@@ -408,6 +449,8 @@ const buildDecorations = (
|
|
|
408
449
|
}
|
|
409
450
|
|
|
410
451
|
let last = range.from;
|
|
452
|
+
let streamingFrom: number | undefined;
|
|
453
|
+
|
|
411
454
|
tree.iterate({
|
|
412
455
|
from: range.from,
|
|
413
456
|
to: range.to,
|
|
@@ -420,17 +463,32 @@ const buildDecorations = (
|
|
|
420
463
|
if (args) {
|
|
421
464
|
const def = registry[args._tag];
|
|
422
465
|
if (def) {
|
|
466
|
+
// Skip unclosed streaming elements — the unclosed tag scan handles them.
|
|
467
|
+
if (def.streaming && !node.node.getChild('CloseTag')) {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
|
|
423
471
|
// NOTE: The widget state may already have been updated before the widget is mounted.
|
|
424
472
|
const { block, factory, Component } = def;
|
|
425
|
-
const widgetState = args.id ? widgetStateMap[args.id] : undefined;
|
|
426
473
|
const nodeRange = { from: node.node.from, to: node.node.to };
|
|
427
|
-
const
|
|
474
|
+
const widgetId = xmlWidgetId(
|
|
475
|
+
args.id,
|
|
476
|
+
def.streaming ? `cm-xml-${nodeRange.from}` : `cm-xml-${nodeRange.from}-${nodeRange.to}`,
|
|
477
|
+
);
|
|
478
|
+
const widgetState = widgetStateMap[widgetId];
|
|
479
|
+
const props = {
|
|
480
|
+
id: widgetId,
|
|
481
|
+
range: nodeRange,
|
|
482
|
+
context,
|
|
483
|
+
...args,
|
|
484
|
+
...widgetState,
|
|
485
|
+
} satisfies XmlWidgetProps;
|
|
428
486
|
|
|
429
487
|
// Create widget.
|
|
430
488
|
const widget: WidgetType | undefined = factory
|
|
431
|
-
? factory(props)
|
|
489
|
+
? (factory(props) ?? undefined)
|
|
432
490
|
: Component
|
|
433
|
-
?
|
|
491
|
+
? new PlaceholderWidget(widgetId, Component, props, notifier)
|
|
434
492
|
: undefined;
|
|
435
493
|
|
|
436
494
|
// Add decoration.
|
|
@@ -463,30 +521,112 @@ const buildDecorations = (
|
|
|
463
521
|
},
|
|
464
522
|
});
|
|
465
523
|
|
|
466
|
-
|
|
524
|
+
// Scan for unclosed streaming tags at the document tail.
|
|
525
|
+
const streamingTagNames = Object.entries(registry)
|
|
526
|
+
.filter(([, def]) => def.streaming)
|
|
527
|
+
.map(([name]) => name)
|
|
528
|
+
// Longest names first so `react-widget` wins over `react` in alternation.
|
|
529
|
+
.sort((a, b) => b.length - a.length);
|
|
530
|
+
|
|
531
|
+
if (streamingTagNames.length > 0) {
|
|
532
|
+
const tailText = state.sliceDoc(range.from, range.to);
|
|
533
|
+
const streamingPattern = streamingTagNames.map(escapeRegExpSource).join('|');
|
|
534
|
+
const tagPattern = new RegExp(`<(${streamingPattern})(\\s[^>]*)?>`, 'g');
|
|
535
|
+
let match: RegExpExecArray | null;
|
|
536
|
+
|
|
537
|
+
while ((match = tagPattern.exec(tailText)) !== null) {
|
|
538
|
+
const tagName = match[1];
|
|
539
|
+
const closeTag = `</${tagName}>`;
|
|
540
|
+
const afterOpen = match.index + match[0].length;
|
|
541
|
+
|
|
542
|
+
// Only process if there's no closing tag after this opening tag.
|
|
543
|
+
if (tailText.indexOf(closeTag, afterOpen) === -1) {
|
|
544
|
+
const absoluteFrom = range.from + match.index;
|
|
545
|
+
const contentFrom = range.from + afterOpen;
|
|
546
|
+
const innerText = state.sliceDoc(contentFrom, range.to).trim();
|
|
547
|
+
|
|
548
|
+
const def = registry[tagName];
|
|
549
|
+
const props: XmlWidgetProps = {
|
|
550
|
+
_tag: tagName,
|
|
551
|
+
context,
|
|
552
|
+
range: { from: absoluteFrom, to: range.to },
|
|
553
|
+
children: innerText ? [innerText] : undefined,
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
// Parse attributes from the opening tag.
|
|
557
|
+
const attrPattern = /(\w+)="([^"]*)"/g;
|
|
558
|
+
let attrMatch: RegExpExecArray | null;
|
|
559
|
+
while ((attrMatch = attrPattern.exec(match[0])) !== null) {
|
|
560
|
+
props[attrMatch[1]] = attrMatch[2];
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const widgetId = xmlWidgetId(props.id, `cm-xml-${absoluteFrom}`);
|
|
564
|
+
const widgetState = widgetStateMap[widgetId];
|
|
565
|
+
const mergedProps = { ...props, id: widgetId, ...widgetState };
|
|
566
|
+
|
|
567
|
+
const widget: WidgetType | undefined = def.factory
|
|
568
|
+
? (def.factory(mergedProps) ?? undefined)
|
|
569
|
+
: def.Component
|
|
570
|
+
? new PlaceholderWidget(widgetId, def.Component, mergedProps, notifier, true)
|
|
571
|
+
: undefined;
|
|
572
|
+
|
|
573
|
+
if (widget) {
|
|
574
|
+
builder.add(
|
|
575
|
+
absoluteFrom,
|
|
576
|
+
range.to,
|
|
577
|
+
Decoration.replace({
|
|
578
|
+
widget,
|
|
579
|
+
block: def.block,
|
|
580
|
+
atomic: true,
|
|
581
|
+
inclusive: true,
|
|
582
|
+
tag: tagName,
|
|
583
|
+
streaming: true,
|
|
584
|
+
contentFrom,
|
|
585
|
+
}),
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
// Set from to just before the streaming tag so next rebuild covers it.
|
|
589
|
+
streamingFrom = absoluteFrom;
|
|
590
|
+
last = absoluteFrom;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Only one streaming tag at a time.
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return { from: last, streamingFrom, decorations: builder.finish() };
|
|
467
600
|
};
|
|
468
601
|
|
|
469
602
|
/**
|
|
470
603
|
* Placeholder for widgets.
|
|
471
604
|
*/
|
|
472
605
|
class PlaceholderWidget<TProps extends XmlWidgetProps> extends WidgetType {
|
|
473
|
-
|
|
606
|
+
#root: HTMLElement | null = null;
|
|
607
|
+
#view: EditorView | undefined;
|
|
474
608
|
|
|
475
609
|
constructor(
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
610
|
+
readonly id: string,
|
|
611
|
+
readonly Component: FunctionComponent<TProps>,
|
|
612
|
+
readonly props: TProps,
|
|
613
|
+
readonly notifier: XmlWidgetNotifier,
|
|
614
|
+
readonly streaming?: boolean,
|
|
480
615
|
) {
|
|
481
616
|
super();
|
|
482
617
|
invariant(id);
|
|
483
618
|
}
|
|
484
619
|
|
|
485
620
|
get root(): HTMLElement | null {
|
|
486
|
-
return this
|
|
621
|
+
return this.#root;
|
|
487
622
|
}
|
|
488
623
|
|
|
489
624
|
override eq(other: this) {
|
|
625
|
+
// Streaming widgets always need updating (content changes on each tick).
|
|
626
|
+
if (this.streaming) {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
|
|
490
630
|
return this.id === other.id;
|
|
491
631
|
}
|
|
492
632
|
|
|
@@ -494,14 +634,25 @@ class PlaceholderWidget<TProps extends XmlWidgetProps> extends WidgetType {
|
|
|
494
634
|
return true;
|
|
495
635
|
}
|
|
496
636
|
|
|
497
|
-
override toDOM(
|
|
498
|
-
this
|
|
499
|
-
|
|
500
|
-
|
|
637
|
+
override toDOM(view: EditorView) {
|
|
638
|
+
this.#view = view;
|
|
639
|
+
// NOTE: Set min-height to avoid jumps while scrolling.
|
|
640
|
+
this.#root = Domino.of('div').classNames('min-h-[24px]').root;
|
|
641
|
+
const props = Object.assign({}, this.props, { view }) as TProps;
|
|
642
|
+
this.notifier.mounted({ id: this.id, root: this.#root, props, Component: this.Component });
|
|
643
|
+
return this.#root;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
override updateDOM(dom: HTMLElement) {
|
|
647
|
+
this.#root = dom;
|
|
648
|
+
const props = Object.assign({}, this.props, { view: this.#view }) as TProps;
|
|
649
|
+
this.notifier.mounted({ id: this.id, root: this.#root, props, Component: this.Component });
|
|
650
|
+
return true;
|
|
501
651
|
}
|
|
502
652
|
|
|
503
653
|
override destroy(_dom: HTMLElement) {
|
|
504
654
|
this.notifier.unmounted(this.id);
|
|
505
|
-
this
|
|
655
|
+
this.#root = null;
|
|
656
|
+
this.#view = undefined;
|
|
506
657
|
}
|
|
507
658
|
}
|