@dxos/ui-editor 0.8.4-main.fcfe5033a5 → 0.9.0
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/LICENSE +102 -5
- package/README.md +1 -1
- package/dist/lib/browser/index.mjs +1258 -1004
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/types/index.mjs +26 -6
- package/dist/lib/browser/types/index.mjs.map +4 -4
- package/dist/lib/node-esm/index.mjs +1258 -1003
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/types/index.mjs +27 -6
- package/dist/lib/node-esm/types/index.mjs.map +4 -4
- package/dist/types/src/defaults.d.ts.map +1 -1
- package/dist/types/src/extensions/annotations.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete/match.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete/placeholder.d.ts +5 -2
- package/dist/types/src/extensions/autocomplete/placeholder.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete/typeahead.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/sync.d.ts +1 -1
- package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -1
- package/dist/types/src/extensions/blast.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +19 -1
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/debug.d.ts.map +1 -1
- package/dist/types/src/extensions/dnd.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts +3 -2
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- 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 +1 -1
- package/dist/types/src/extensions/index.d.ts +3 -4
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/json.d.ts +1 -1
- package/dist/types/src/extensions/json.d.ts.map +1 -1
- package/dist/types/src/extensions/listener.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/changes.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/debug.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/image.d.ts +13 -2
- package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/image.test.d.ts +2 -0
- package/dist/types/src/extensions/markdown/image.test.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/table.d.ts.map +1 -1
- package/dist/types/src/extensions/mention.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner/menu.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner/outliner.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner/selection.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner/tree.d.ts.map +1 -1
- package/dist/types/src/extensions/preview/preview.d.ts +2 -2
- package/dist/types/src/extensions/preview/preview.d.ts.map +1 -1
- package/dist/types/src/extensions/replacer.d.ts.map +1 -1
- package/dist/types/src/extensions/scrolling/auto-scroll.d.ts +18 -0
- package/dist/types/src/extensions/scrolling/auto-scroll.d.ts.map +1 -0
- package/dist/types/src/extensions/scrolling/crawler.d.ts +83 -0
- package/dist/types/src/extensions/scrolling/crawler.d.ts.map +1 -0
- package/dist/types/src/extensions/scrolling/index.d.ts +6 -0
- package/dist/types/src/extensions/scrolling/index.d.ts.map +1 -0
- package/dist/types/src/extensions/scrolling/scroll-past-end.d.ts.map +1 -0
- package/dist/types/src/extensions/scrolling/scrollbar-autohide.d.ts +15 -0
- package/dist/types/src/extensions/scrolling/scrollbar-autohide.d.ts.map +1 -0
- package/dist/types/src/extensions/scrolling/scroller.d.ts +16 -0
- package/dist/types/src/extensions/scrolling/scroller.d.ts.map +1 -0
- package/dist/types/src/extensions/selection.d.ts.map +1 -1
- 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/spacing.d.ts +3 -0
- package/dist/types/src/extensions/spacing.d.ts.map +1 -0
- package/dist/types/src/extensions/submit.d.ts.map +1 -1
- package/dist/types/src/extensions/tags/extended-markdown.d.ts.map +1 -1
- package/dist/types/src/extensions/tags/fader.d.ts.map +1 -1
- package/dist/types/src/extensions/tags/index.d.ts +3 -1
- package/dist/types/src/extensions/tags/index.d.ts.map +1 -1
- 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 +1 -8
- package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -1
- package/dist/types/src/extensions/tags/xml-util.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +0 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/theme.d.ts.map +1 -1
- package/dist/types/src/types/types.d.ts +2 -2
- package/dist/types/src/types/types.d.ts.map +1 -1
- package/dist/types/src/util/cursor.d.ts.map +1 -1
- package/dist/types/src/util/debug.d.ts.map +1 -1
- package/dist/types/src/util/decorations.d.ts.map +1 -1
- package/dist/types/src/util/dom.d.ts.map +1 -1
- package/dist/types/src/util/facet.d.ts.map +1 -1
- package/dist/types/src/util/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +55 -57
- package/src/defaults.ts +6 -4
- package/src/extensions/autocomplete/placeholder.ts +37 -18
- package/src/extensions/automerge/automerge.test.tsx +35 -9
- package/src/extensions/automerge/automerge.ts +1 -1
- package/src/extensions/automerge/cursor.ts +1 -1
- package/src/extensions/automerge/sync.ts +1 -1
- package/src/extensions/automerge/update-automerge.ts +1 -1
- package/src/extensions/comments.ts +54 -31
- package/src/extensions/factories.test.ts +88 -0
- package/src/extensions/factories.ts +22 -4
- package/src/extensions/index.ts +3 -4
- package/src/extensions/json.ts +1 -1
- package/src/extensions/markdown/decorate.ts +1 -1
- package/src/extensions/markdown/image.test.ts +54 -0
- package/src/extensions/markdown/image.ts +70 -9
- package/src/extensions/markdown/link.ts +7 -2
- package/src/extensions/outliner/outliner.ts +1 -1
- package/src/extensions/preview/preview.ts +14 -12
- package/src/extensions/scrolling/auto-scroll.ts +261 -0
- package/src/extensions/{scroller.ts → scrolling/crawler.ts} +89 -48
- package/src/extensions/scrolling/index.ts +9 -0
- package/src/extensions/{scroll-past-end.ts → scrolling/scroll-past-end.ts} +6 -6
- package/src/extensions/scrolling/scrollbar-autohide.ts +61 -0
- package/src/extensions/scrolling/scroller.ts +27 -0
- package/src/extensions/snippets.ts +67 -0
- package/src/extensions/spacing.ts +15 -0
- package/src/extensions/tags/index.ts +3 -1
- package/src/extensions/tags/testing/text.md +36 -0
- package/src/extensions/tags/testing/text.txt +35 -0
- package/src/extensions/tags/{wire.test.ts → typewriter.test.ts} +2 -2
- 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 +6 -32
- package/src/extensions/tags/xml-util.test.ts +90 -3
- package/src/extensions/tags/xml-util.ts +62 -5
- package/src/index.ts +0 -1
- package/src/styles/theme.ts +23 -13
- package/src/typings.d.ts +8 -0
- package/dist/lib/browser/chunk-D724USEC.mjs +0 -34
- package/dist/lib/browser/chunk-D724USEC.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-JRVJWKQF.mjs +0 -36
- package/dist/lib/node-esm/chunk-JRVJWKQF.mjs.map +0 -7
- package/dist/types/src/extensions/auto-scroll.d.ts +0 -8
- package/dist/types/src/extensions/auto-scroll.d.ts.map +0 -1
- package/dist/types/src/extensions/scroll-past-end.d.ts.map +0 -1
- package/dist/types/src/extensions/scroller.d.ts +0 -63
- package/dist/types/src/extensions/scroller.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/wire.d.ts +0 -23
- package/dist/types/src/extensions/tags/wire.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/wire.test.d.ts +0 -2
- package/dist/types/src/extensions/tags/wire.test.d.ts.map +0 -1
- package/dist/types/src/extensions/typewriter.d.ts +0 -10
- package/dist/types/src/extensions/typewriter.d.ts.map +0 -1
- package/src/extensions/auto-scroll.ts +0 -179
- package/src/extensions/tags/wire.ts +0 -459
- package/src/extensions/typewriter.ts +0 -68
- /package/dist/types/src/extensions/{scroll-past-end.d.ts → scrolling/scroll-past-end.d.ts} +0 -0
|
@@ -1,459 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2025 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
Annotation,
|
|
7
|
-
ChangeSet,
|
|
8
|
-
EditorState,
|
|
9
|
-
type Extension,
|
|
10
|
-
StateEffect,
|
|
11
|
-
StateField,
|
|
12
|
-
type Transaction,
|
|
13
|
-
} from '@codemirror/state';
|
|
14
|
-
import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate, WidgetType } from '@codemirror/view';
|
|
15
|
-
|
|
16
|
-
/** Annotate a transaction to bypass the wire buffer (content appears immediately). */
|
|
17
|
-
export const wireBypass = Annotation.define<boolean>();
|
|
18
|
-
|
|
19
|
-
import { Domino } from '@dxos/ui';
|
|
20
|
-
|
|
21
|
-
type BufferState = { text: string; insertAt: number };
|
|
22
|
-
|
|
23
|
-
const DEFAULT_RATE = 200;
|
|
24
|
-
const CURSOR_LINGER = 3_000;
|
|
25
|
-
|
|
26
|
-
export type WireOptions = {
|
|
27
|
-
/** Characters per second. */
|
|
28
|
-
rate?: number;
|
|
29
|
-
/** Show a blinking cursor at the insertion point while streaming. */
|
|
30
|
-
cursor?: boolean;
|
|
31
|
-
/** Tag names whose inner content should be streamed (not buffered until close). */
|
|
32
|
-
streamingTags?: Set<string>;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Extension that intercepts appended text and inserts it one character at a time,
|
|
37
|
-
* except for XML tags, links, and images which are flushed atomically.
|
|
38
|
-
*/
|
|
39
|
-
export const wire = (options: WireOptions = {}): Extension => {
|
|
40
|
-
const rate = options.rate ?? DEFAULT_RATE;
|
|
41
|
-
const interval = 1_000 / rate;
|
|
42
|
-
const streamingTags = options.streamingTags ?? new Set<string>();
|
|
43
|
-
|
|
44
|
-
// Effect to suppress a transaction from being applied (replaced by buffered insert).
|
|
45
|
-
const suppressAppend = StateEffect.define<{ from: number; text: string }>();
|
|
46
|
-
// Effect to insert text from the buffer (single char or atomic chunk).
|
|
47
|
-
const insertChunk = StateEffect.define<{ from: number; text: string }>();
|
|
48
|
-
|
|
49
|
-
// State field that holds the pending buffer of text to drip into the document.
|
|
50
|
-
const bufferField = StateField.define<BufferState>({
|
|
51
|
-
create: () => ({ text: '', insertAt: 0 }),
|
|
52
|
-
update: (value, tr) => {
|
|
53
|
-
let { text, insertAt } = value;
|
|
54
|
-
|
|
55
|
-
for (const effect of tr.effects) {
|
|
56
|
-
if (effect.is(suppressAppend)) {
|
|
57
|
-
text += effect.value.text;
|
|
58
|
-
if (text.length === effect.value.text.length) {
|
|
59
|
-
insertAt = effect.value.from;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (effect.is(insertChunk)) {
|
|
63
|
-
text = text.slice(effect.value.text.length);
|
|
64
|
-
insertAt = effect.value.from + effect.value.text.length;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Reset buffer when document is cleared or fully replaced.
|
|
69
|
-
if (tr.docChanged) {
|
|
70
|
-
let isReset = tr.state.doc.length === 0;
|
|
71
|
-
if (!isReset && tr.startState.doc.length > 0) {
|
|
72
|
-
tr.changes.iterChanges((fromA, toA) => {
|
|
73
|
-
if (fromA === 0 && toA === tr.startState.doc.length) {
|
|
74
|
-
isReset = true;
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
if (isReset) {
|
|
79
|
-
return { text: '', insertAt: 0 };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Map insertion position through document changes not caused by us.
|
|
83
|
-
if (!tr.effects.some((effect) => effect.is(insertChunk))) {
|
|
84
|
-
insertAt = tr.changes.mapPos(Math.min(insertAt, tr.startState.doc.length));
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return { text, insertAt };
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
// Transaction filter: intercept appends at end-of-document and buffer them.
|
|
93
|
-
const filter = EditorState.transactionFilter.of((tr: Transaction) => {
|
|
94
|
-
if (!tr.docChanged) {
|
|
95
|
-
return tr;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Allow bypassed and drip insertions through.
|
|
99
|
-
if (tr.annotation(wireBypass) || tr.effects.some((effect) => effect.is(insertChunk))) {
|
|
100
|
-
return tr;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Collect appended text at the end of the document.
|
|
104
|
-
let appendedText = '';
|
|
105
|
-
let appendFrom = -1;
|
|
106
|
-
let isAppendOnly = true;
|
|
107
|
-
|
|
108
|
-
tr.changes.iterChanges((fromA, toA, _fromB, _toB, inserted) => {
|
|
109
|
-
if (toA === tr.startState.doc.length && fromA === toA && inserted.length > 0) {
|
|
110
|
-
appendedText += inserted.sliceString(0);
|
|
111
|
-
if (appendFrom === -1) {
|
|
112
|
-
appendFrom = fromA;
|
|
113
|
-
}
|
|
114
|
-
} else {
|
|
115
|
-
isAppendOnly = false;
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
if (!isAppendOnly || appendedText.length === 0) {
|
|
120
|
-
return tr;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Suppress the original insert; buffer the text instead.
|
|
124
|
-
return {
|
|
125
|
-
changes: ChangeSet.empty(tr.startState.doc.length),
|
|
126
|
-
effects: suppressAppend.of({ from: appendFrom, text: appendedText }),
|
|
127
|
-
};
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// View plugin that drains the buffer, emitting one character or one atomic chunk per tick.
|
|
131
|
-
const drainPlugin = ViewPlugin.fromClass(
|
|
132
|
-
class {
|
|
133
|
-
#timer: ReturnType<typeof setInterval> | undefined;
|
|
134
|
-
#activeStreamTag: string | null = null;
|
|
135
|
-
|
|
136
|
-
constructor(private view: EditorView) {
|
|
137
|
-
this.#start();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
update(update: ViewUpdate) {
|
|
141
|
-
const buffer = update.state.field(bufferField);
|
|
142
|
-
// Reset streaming state when buffer is cleared (e.g., document reset).
|
|
143
|
-
if (buffer.text.length === 0) {
|
|
144
|
-
this.#activeStreamTag = null;
|
|
145
|
-
}
|
|
146
|
-
if (buffer.text.length > 0 && this.#timer === undefined) {
|
|
147
|
-
this.#start();
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
#start() {
|
|
152
|
-
this.#timer = setInterval(() => {
|
|
153
|
-
const { text, insertAt } = this.view.state.field(bufferField);
|
|
154
|
-
if (text.length === 0) {
|
|
155
|
-
clearInterval(this.#timer);
|
|
156
|
-
this.#timer = undefined;
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const result = flushable(text, streamingTags, this.#activeStreamTag);
|
|
161
|
-
if (result.count === 0) {
|
|
162
|
-
// Structure incomplete — wait for more data.
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (result.enterTag) {
|
|
167
|
-
this.#activeStreamTag = result.enterTag;
|
|
168
|
-
}
|
|
169
|
-
if (result.exitTag) {
|
|
170
|
-
this.#activeStreamTag = null;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const chunk = text.slice(0, result.count);
|
|
174
|
-
this.view.dispatch({
|
|
175
|
-
changes: { from: insertAt, insert: chunk },
|
|
176
|
-
effects: insertChunk.of({ from: insertAt, text: chunk }),
|
|
177
|
-
});
|
|
178
|
-
}, interval);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
destroy() {
|
|
182
|
-
clearInterval(this.#timer);
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
return [bufferField, filter, drainPlugin, options.cursor && wireCursor(bufferField)].filter(Boolean) as Extension[];
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
//
|
|
191
|
-
// Cursor
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Blinking cursor widget at the insertion point while the buffer is draining.
|
|
196
|
-
* Lingers for 2s after the buffer empties before being removed.
|
|
197
|
-
*/
|
|
198
|
-
const wireCursor = (bufferField: StateField<BufferState>): Extension => {
|
|
199
|
-
const hideCursor = StateEffect.define();
|
|
200
|
-
|
|
201
|
-
const visibilityField = StateField.define<{
|
|
202
|
-
visible: boolean;
|
|
203
|
-
insertAt: number;
|
|
204
|
-
/** Position after the last non-whitespace character was inserted. */
|
|
205
|
-
lastNonWsAt: number;
|
|
206
|
-
}>({
|
|
207
|
-
create: () => ({ visible: false, insertAt: 0, lastNonWsAt: 0 }),
|
|
208
|
-
update: (value, tr) => {
|
|
209
|
-
const { text, insertAt } = tr.state.field(bufferField);
|
|
210
|
-
if (text.length > 0) {
|
|
211
|
-
// Track the last position where a non-whitespace character was inserted.
|
|
212
|
-
let lastNonWsAt = tr.changes.mapPos(Math.min(value.lastNonWsAt, tr.startState.doc.length));
|
|
213
|
-
if (tr.docChanged) {
|
|
214
|
-
tr.changes.iterChanges((_fromA, _toA, _fromB, _toB, inserted) => {
|
|
215
|
-
const chunk = inserted.sliceString(0);
|
|
216
|
-
if (chunk.trim().length > 0) {
|
|
217
|
-
lastNonWsAt = _fromB + chunk.length;
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
return { visible: true, insertAt, lastNonWsAt };
|
|
222
|
-
}
|
|
223
|
-
for (const effect of tr.effects) {
|
|
224
|
-
if (effect.is(hideCursor)) {
|
|
225
|
-
return { ...value, visible: false };
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return value;
|
|
229
|
-
},
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
const decorationField = StateField.define<DecorationSet>({
|
|
233
|
-
create: () => Decoration.none,
|
|
234
|
-
update: (_decorations, tr) => {
|
|
235
|
-
const { visible, insertAt, lastNonWsAt } = tr.state.field(visibilityField);
|
|
236
|
-
if (!visible) {
|
|
237
|
-
return Decoration.none;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const { text } = tr.state.field(bufferField);
|
|
241
|
-
// While draining, show cursor at the insertion point.
|
|
242
|
-
// When lingering (buffer empty), show at last non-whitespace position.
|
|
243
|
-
const cursorAt = text.length > 0 ? insertAt : lastNonWsAt;
|
|
244
|
-
const pos = Math.min(cursorAt, tr.state.doc.length);
|
|
245
|
-
return Decoration.set([
|
|
246
|
-
Decoration.widget({
|
|
247
|
-
widget: new CursorWidget(),
|
|
248
|
-
side: 1,
|
|
249
|
-
}).range(pos),
|
|
250
|
-
]);
|
|
251
|
-
},
|
|
252
|
-
provide: (field) => EditorView.decorations.from(field),
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
const timerPlugin = ViewPlugin.fromClass(
|
|
256
|
-
class {
|
|
257
|
-
#timer: ReturnType<typeof setTimeout> | undefined;
|
|
258
|
-
|
|
259
|
-
constructor(private view: EditorView) {}
|
|
260
|
-
|
|
261
|
-
update(update: ViewUpdate) {
|
|
262
|
-
const { text } = update.state.field(bufferField);
|
|
263
|
-
const { visible } = update.state.field(visibilityField);
|
|
264
|
-
|
|
265
|
-
if (text.length > 0) {
|
|
266
|
-
clearTimeout(this.#timer);
|
|
267
|
-
this.#timer = undefined;
|
|
268
|
-
} else if (visible && this.#timer === undefined) {
|
|
269
|
-
this.#timer = setTimeout(() => {
|
|
270
|
-
this.view.dispatch({ effects: hideCursor.of(null) });
|
|
271
|
-
this.#timer = undefined;
|
|
272
|
-
}, CURSOR_LINGER);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
destroy() {
|
|
277
|
-
clearTimeout(this.#timer);
|
|
278
|
-
}
|
|
279
|
-
},
|
|
280
|
-
);
|
|
281
|
-
|
|
282
|
-
return [visibilityField, decorationField, timerPlugin];
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* U+2217 Asterisk
|
|
287
|
-
* U+25CF Ballot Box
|
|
288
|
-
*/
|
|
289
|
-
class CursorWidget extends WidgetType {
|
|
290
|
-
toDOM() {
|
|
291
|
-
const inner = Domino.of('span').text('\u2217').style({ animation: 'blink 1s infinite', animationDelay: '250ms' });
|
|
292
|
-
return Domino.of('span').style({ opacity: '0.8' }).append(inner).root;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Matches the local name of an opening tag after `<` (not `</`).
|
|
298
|
-
* Custom elements use hyphens; `\w+` alone incorrectly stops at `-` (e.g. `dom` from `dom-widget`).
|
|
299
|
-
*/
|
|
300
|
-
const OPENING_TAG_NAME = /^<([a-zA-Z][\w-]*)/;
|
|
301
|
-
|
|
302
|
-
/** Escapes a string for safe embedding in RegExp source (tag names from the document). */
|
|
303
|
-
const escapeRegExpSource = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
304
|
-
|
|
305
|
-
type FlushResult = {
|
|
306
|
-
count: number;
|
|
307
|
-
/** Tag name to enter streaming mode for. */
|
|
308
|
-
enterTag?: string;
|
|
309
|
-
/** Whether to exit streaming mode. */
|
|
310
|
-
exitTag?: boolean;
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Scans the buffer and returns the number of characters that can be flushed.
|
|
315
|
-
* Returns 0 if the head of the buffer is inside an incomplete structure
|
|
316
|
-
* (XML element, markdown link, or image) that should be flushed atomically.
|
|
317
|
-
* Returns > 1 when a complete structure is at the head and should be emitted in one batch.
|
|
318
|
-
*
|
|
319
|
-
* When `activeStreamTag` is set, we're inside a streaming tag: inner content drips
|
|
320
|
-
* one character at a time, and the closing tag is flushed atomically.
|
|
321
|
-
*/
|
|
322
|
-
const flushable = (buffer: string, streamingTags: Set<string>, activeStreamTag: string | null): FlushResult => {
|
|
323
|
-
if (buffer.length === 0) {
|
|
324
|
-
return { count: 0 };
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Inside a streaming tag: drip content, flush closing tag atomically.
|
|
328
|
-
if (activeStreamTag) {
|
|
329
|
-
const closeTag = `</${activeStreamTag}>`;
|
|
330
|
-
if (buffer.startsWith(closeTag)) {
|
|
331
|
-
return { count: closeTag.length, exitTag: true };
|
|
332
|
-
}
|
|
333
|
-
// Nested XML element — buffer atomically.
|
|
334
|
-
if (buffer[0] === '<') {
|
|
335
|
-
return { count: xmlElementLength(buffer) };
|
|
336
|
-
}
|
|
337
|
-
// Drip inner content one character at a time.
|
|
338
|
-
return { count: 1 };
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const ch = buffer[0];
|
|
342
|
-
|
|
343
|
-
// XML element.
|
|
344
|
-
if (ch === '<') {
|
|
345
|
-
// Check if this is a streaming tag's opening tag.
|
|
346
|
-
const nameMatch = buffer.match(OPENING_TAG_NAME);
|
|
347
|
-
if (nameMatch && streamingTags.has(nameMatch[1])) {
|
|
348
|
-
const close = buffer.indexOf('>');
|
|
349
|
-
if (close === -1) {
|
|
350
|
-
return { count: 0 }; // Opening tag incomplete.
|
|
351
|
-
}
|
|
352
|
-
// Self-closing streaming tag — flush atomically, no streaming mode.
|
|
353
|
-
if (buffer[close - 1] === '/') {
|
|
354
|
-
return { count: close + 1 };
|
|
355
|
-
}
|
|
356
|
-
// Flush opening tag and enter streaming mode.
|
|
357
|
-
return { count: close + 1, enterTag: nameMatch[1] };
|
|
358
|
-
}
|
|
359
|
-
// Non-streaming XML: buffer the entire element.
|
|
360
|
-
return { count: xmlElementLength(buffer) };
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Image:  — starts with '!'.
|
|
364
|
-
if (ch === '!' && buffer.length > 1 && buffer[1] === '[') {
|
|
365
|
-
return { count: linkLength(buffer, 1) };
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Link: [text](url).
|
|
369
|
-
if (ch === '[') {
|
|
370
|
-
return { count: linkLength(buffer, 0) };
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
return { count: 1 };
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Returns the length of a complete XML element at the start of the buffer, or 0 if the element is incomplete.
|
|
378
|
-
* Handles self-closing tags, closing tags, and opening tags with nested content.
|
|
379
|
-
* E.g., `<foo>content<bar />more</foo>` returns the full length.
|
|
380
|
-
*/
|
|
381
|
-
export const xmlElementLength = (buffer: string): number => {
|
|
382
|
-
const close = buffer.indexOf('>');
|
|
383
|
-
if (close === -1) {
|
|
384
|
-
return 0; // Tag not closed yet.
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// Self-closing tag: <foo />.
|
|
388
|
-
if (buffer[close - 1] === '/') {
|
|
389
|
-
return close + 1;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Closing tag: </foo>.
|
|
393
|
-
if (buffer[1] === '/') {
|
|
394
|
-
return close + 1;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Opening tag: extract the tag name and find its matching closing tag.
|
|
398
|
-
const nameMatch = buffer.match(OPENING_TAG_NAME);
|
|
399
|
-
if (!nameMatch) {
|
|
400
|
-
// Not a valid tag (e.g., `< ` or `<123`); emit one character.
|
|
401
|
-
return 1;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
const tagName = nameMatch[1];
|
|
405
|
-
let depth = 0;
|
|
406
|
-
|
|
407
|
-
// Walk through all tags in the buffer tracking nesting depth.
|
|
408
|
-
const tagPattern = new RegExp(`<(/?)${escapeRegExpSource(tagName)}(\\s[^>]*)?>`, 'g');
|
|
409
|
-
let match: RegExpExecArray | null;
|
|
410
|
-
while ((match = tagPattern.exec(buffer)) !== null) {
|
|
411
|
-
const isSelfClosing = match[0].endsWith('/>');
|
|
412
|
-
const isClosing = match[1] === '/';
|
|
413
|
-
|
|
414
|
-
if (isSelfClosing) {
|
|
415
|
-
// Self-closing doesn't change depth, but if depth is 0 this is the root.
|
|
416
|
-
if (depth === 0) {
|
|
417
|
-
return match.index + match[0].length;
|
|
418
|
-
}
|
|
419
|
-
} else if (isClosing) {
|
|
420
|
-
depth--;
|
|
421
|
-
if (depth === 0) {
|
|
422
|
-
return match.index + match[0].length;
|
|
423
|
-
}
|
|
424
|
-
} else {
|
|
425
|
-
depth++;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Unbalanced — still waiting for closing tag.
|
|
430
|
-
return 0;
|
|
431
|
-
};
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* Returns the length of a complete markdown link/image starting at `offset`,
|
|
435
|
-
* or 0 if the structure is incomplete.
|
|
436
|
-
* Expects buffer[offset] === '['.
|
|
437
|
-
*/
|
|
438
|
-
const linkLength = (buffer: string, offset: number): number => {
|
|
439
|
-
const bracketClose = buffer.indexOf(']', offset + 1);
|
|
440
|
-
if (bracketClose === -1) {
|
|
441
|
-
return 0;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// Must be followed by '(' for a standard link.
|
|
445
|
-
if (bracketClose + 1 >= buffer.length) {
|
|
446
|
-
return 0;
|
|
447
|
-
}
|
|
448
|
-
if (buffer[bracketClose + 1] !== '(') {
|
|
449
|
-
// Not a link — just a bracket; emit one character.
|
|
450
|
-
return 1;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
const parenClose = buffer.indexOf(')', bracketClose + 2);
|
|
454
|
-
if (parenClose === -1) {
|
|
455
|
-
return 0;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
return parenClose + 1;
|
|
459
|
-
};
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2023 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { type Extension } from '@codemirror/state';
|
|
6
|
-
import { keymap } from '@codemirror/view';
|
|
7
|
-
|
|
8
|
-
// TODO(burdon): Review https://github.com/sergeche/codemirror-movie?tab=readme-ov-file
|
|
9
|
-
|
|
10
|
-
export type DemoOptions = {
|
|
11
|
-
delay?: number;
|
|
12
|
-
items?: string[];
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const defaultItems = ['hello world!', 'this is a test.', 'this is [DXOS](https://dxos.org)'];
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Configurable plugin that let's user cycle through pre-configured input script.
|
|
19
|
-
*/
|
|
20
|
-
export const typewriter = ({ delay = 75, items = defaultItems }: DemoOptions = {}): Extension => {
|
|
21
|
-
let t: any;
|
|
22
|
-
let idx = 0; // TODO(burdon): Make global.
|
|
23
|
-
|
|
24
|
-
return [
|
|
25
|
-
keymap.of([
|
|
26
|
-
{
|
|
27
|
-
// Reset.
|
|
28
|
-
key: "alt-meta-'",
|
|
29
|
-
run: () => {
|
|
30
|
-
clearTimeout(t);
|
|
31
|
-
idx = 0;
|
|
32
|
-
return true;
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
// Next prompt.
|
|
37
|
-
// TODO(burdon): Press 1-9 to select prompt?
|
|
38
|
-
key: "Shift-Meta-'",
|
|
39
|
-
run: (view) => {
|
|
40
|
-
clearTimeout(t);
|
|
41
|
-
// TODO(burdon): Add space if needed.
|
|
42
|
-
const text = items[idx++];
|
|
43
|
-
if (idx === items?.length) {
|
|
44
|
-
idx = 0;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
let i = 0;
|
|
48
|
-
const insert = (d = 0) => {
|
|
49
|
-
t = setTimeout(() => {
|
|
50
|
-
const pos = view.state.selection.main.head;
|
|
51
|
-
view.dispatch({
|
|
52
|
-
changes: { from: pos, insert: text[i++] },
|
|
53
|
-
selection: { anchor: pos + 1 },
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
if (i < text.length) {
|
|
57
|
-
insert(Math.random() * delay * (text[i] === ' ' ? 2 : 1));
|
|
58
|
-
}
|
|
59
|
-
}, d);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
insert();
|
|
63
|
-
return true;
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
]),
|
|
67
|
-
];
|
|
68
|
-
};
|
|
File without changes
|