@difizen/libro-codemirror 0.0.2-alpha.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 +21 -0
- package/README.md +0 -0
- package/es/auto-complete/closebrackets.d.ts +12 -0
- package/es/auto-complete/closebrackets.d.ts.map +1 -0
- package/es/auto-complete/closebrackets.js +408 -0
- package/es/auto-complete/completion.d.ts +57 -0
- package/es/auto-complete/completion.d.ts.map +1 -0
- package/es/auto-complete/completion.js +265 -0
- package/es/auto-complete/config.d.ts +22 -0
- package/es/auto-complete/config.d.ts.map +1 -0
- package/es/auto-complete/config.js +44 -0
- package/es/auto-complete/filter.d.ts +13 -0
- package/es/auto-complete/filter.d.ts.map +1 -0
- package/es/auto-complete/filter.js +191 -0
- package/es/auto-complete/index.d.ts +17 -0
- package/es/auto-complete/index.d.ts.map +1 -0
- package/es/auto-complete/index.js +107 -0
- package/es/auto-complete/snippet.d.ts +14 -0
- package/es/auto-complete/snippet.d.ts.map +1 -0
- package/es/auto-complete/snippet.js +447 -0
- package/es/auto-complete/state.d.ts +63 -0
- package/es/auto-complete/state.d.ts.map +1 -0
- package/es/auto-complete/state.js +452 -0
- package/es/auto-complete/theme.d.ts +6 -0
- package/es/auto-complete/theme.d.ts.map +1 -0
- package/es/auto-complete/theme.js +151 -0
- package/es/auto-complete/tooltip.d.ts +5 -0
- package/es/auto-complete/tooltip.d.ts.map +1 -0
- package/es/auto-complete/tooltip.js +365 -0
- package/es/auto-complete/view.d.ts +43 -0
- package/es/auto-complete/view.d.ts.map +1 -0
- package/es/auto-complete/view.js +372 -0
- package/es/auto-complete/word.d.ts +3 -0
- package/es/auto-complete/word.d.ts.map +1 -0
- package/es/auto-complete/word.js +119 -0
- package/es/completion.d.ts +6 -0
- package/es/completion.d.ts.map +1 -0
- package/es/completion.js +84 -0
- package/es/config.d.ts +184 -0
- package/es/config.d.ts.map +1 -0
- package/es/config.js +473 -0
- package/es/editor.d.ts +361 -0
- package/es/editor.d.ts.map +1 -0
- package/es/editor.js +1126 -0
- package/es/factory.d.ts +3 -0
- package/es/factory.d.ts.map +1 -0
- package/es/factory.js +12 -0
- package/es/hyperlink.d.ts +15 -0
- package/es/hyperlink.d.ts.map +1 -0
- package/es/hyperlink.js +120 -0
- package/es/indent.d.ts +8 -0
- package/es/indent.d.ts.map +1 -0
- package/es/indent.js +58 -0
- package/es/indentation-markers/config.d.ts +17 -0
- package/es/indentation-markers/config.d.ts.map +1 -0
- package/es/indentation-markers/config.js +10 -0
- package/es/indentation-markers/index.d.ts +3 -0
- package/es/indentation-markers/index.d.ts.map +1 -0
- package/es/indentation-markers/index.js +160 -0
- package/es/indentation-markers/map.d.ts +77 -0
- package/es/indentation-markers/map.d.ts.map +1 -0
- package/es/indentation-markers/map.js +265 -0
- package/es/indentation-markers/utils.d.ts +27 -0
- package/es/indentation-markers/utils.d.ts.map +1 -0
- package/es/indentation-markers/utils.js +91 -0
- package/es/index.d.ts +11 -0
- package/es/index.d.ts.map +1 -0
- package/es/index.js +10 -0
- package/es/libro-icon.d.ts +3 -0
- package/es/libro-icon.d.ts.map +1 -0
- package/es/libro-icon.js +2 -0
- package/es/mimetype.d.ts +22 -0
- package/es/mimetype.d.ts.map +1 -0
- package/es/mimetype.js +59 -0
- package/es/mode.d.ts +86 -0
- package/es/mode.d.ts.map +1 -0
- package/es/mode.js +284 -0
- package/es/monitor.d.ts +32 -0
- package/es/monitor.d.ts.map +1 -0
- package/es/monitor.js +129 -0
- package/es/python-lang.d.ts +3 -0
- package/es/python-lang.d.ts.map +1 -0
- package/es/python-lang.js +7 -0
- package/es/style/base.css +131 -0
- package/es/style/theme.css +12 -0
- package/es/style/variables.css +403 -0
- package/es/theme.d.ts +35 -0
- package/es/theme.d.ts.map +1 -0
- package/es/theme.js +225 -0
- package/es/tooltip.d.ts +10 -0
- package/es/tooltip.d.ts.map +1 -0
- package/es/tooltip.js +170 -0
- package/package.json +74 -0
- package/src/auto-complete/README.md +71 -0
- package/src/auto-complete/closebrackets.ts +423 -0
- package/src/auto-complete/completion.ts +345 -0
- package/src/auto-complete/config.ts +101 -0
- package/src/auto-complete/filter.ts +215 -0
- package/src/auto-complete/index.ts +112 -0
- package/src/auto-complete/snippet.ts +394 -0
- package/src/auto-complete/state.ts +472 -0
- package/src/auto-complete/theme.ts +126 -0
- package/src/auto-complete/tooltip.ts +386 -0
- package/src/auto-complete/view.ts +343 -0
- package/src/auto-complete/word.ts +118 -0
- package/src/completion.ts +61 -0
- package/src/config.ts +689 -0
- package/src/editor.ts +1078 -0
- package/src/factory.ts +10 -0
- package/src/hyperlink.ts +95 -0
- package/src/indent.ts +69 -0
- package/src/indentation-markers/config.ts +31 -0
- package/src/indentation-markers/index.ts +192 -0
- package/src/indentation-markers/map.ts +273 -0
- package/src/indentation-markers/utils.ts +84 -0
- package/src/index.ts +11 -0
- package/src/libro-icon.ts +4 -0
- package/src/mimetype.ts +49 -0
- package/src/mode.ts +269 -0
- package/src/monitor.ts +105 -0
- package/src/python-lang.ts +7 -0
- package/src/style/base.css +129 -0
- package/src/style/theme.css +12 -0
- package/src/style/variables.css +405 -0
- package/src/theme.ts +231 -0
- package/src/tooltip.ts +145 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-parameter-properties */
|
|
2
|
+
/* eslint-disable @typescript-eslint/parameter-properties */
|
|
3
|
+
import type { Transaction } from '@codemirror/state';
|
|
4
|
+
import type {
|
|
5
|
+
EditorView,
|
|
6
|
+
Command,
|
|
7
|
+
PluginValue,
|
|
8
|
+
ViewUpdate,
|
|
9
|
+
TooltipView,
|
|
10
|
+
} from '@codemirror/view';
|
|
11
|
+
import { ViewPlugin, logException, getTooltip } from '@codemirror/view';
|
|
12
|
+
|
|
13
|
+
import type { CompletionResult } from './completion.js';
|
|
14
|
+
import { cur, CompletionContext, applyCompletion } from './completion.js';
|
|
15
|
+
import { completionConfig } from './config.js';
|
|
16
|
+
import {
|
|
17
|
+
completionState,
|
|
18
|
+
setSelectedEffect,
|
|
19
|
+
startCompletionEffect,
|
|
20
|
+
closeCompletionEffect,
|
|
21
|
+
setActiveEffect,
|
|
22
|
+
State,
|
|
23
|
+
ActiveSource,
|
|
24
|
+
ActiveResult,
|
|
25
|
+
getUserEvent,
|
|
26
|
+
} from './state.js';
|
|
27
|
+
|
|
28
|
+
/// Returns a command that moves the completion selection forward or
|
|
29
|
+
/// backward by the given amount.
|
|
30
|
+
export function moveCompletionSelection(
|
|
31
|
+
forward: boolean,
|
|
32
|
+
by: 'option' | 'page' = 'option',
|
|
33
|
+
): Command {
|
|
34
|
+
return (view: EditorView) => {
|
|
35
|
+
const cState = view.state.field(completionState, false);
|
|
36
|
+
if (
|
|
37
|
+
!cState ||
|
|
38
|
+
!cState.open ||
|
|
39
|
+
Date.now() - cState.open.timestamp <
|
|
40
|
+
view.state.facet(completionConfig).interactionDelay
|
|
41
|
+
) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
let step = 1,
|
|
45
|
+
tooltip: TooltipView | null;
|
|
46
|
+
if (by === 'page' && (tooltip = getTooltip(view, cState.open.tooltip))) {
|
|
47
|
+
step = Math.max(
|
|
48
|
+
2,
|
|
49
|
+
Math.floor(
|
|
50
|
+
tooltip.dom.offsetHeight /
|
|
51
|
+
(tooltip.dom.querySelector('li') as HTMLElement).offsetHeight,
|
|
52
|
+
) - 1,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
const { length } = cState.open.options;
|
|
56
|
+
let selected =
|
|
57
|
+
cState.open.selected > -1
|
|
58
|
+
? cState.open.selected + step * (forward ? 1 : -1)
|
|
59
|
+
: forward
|
|
60
|
+
? 0
|
|
61
|
+
: length - 1;
|
|
62
|
+
if (selected < 0) {
|
|
63
|
+
selected = by === 'page' ? 0 : length - 1;
|
|
64
|
+
} else if (selected >= length) {
|
|
65
|
+
selected = by === 'page' ? length - 1 : 0;
|
|
66
|
+
}
|
|
67
|
+
view.dispatch({ effects: setSelectedEffect.of(selected) });
|
|
68
|
+
return true;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Accept the current completion.
|
|
73
|
+
export const acceptCompletion: Command = (view: EditorView) => {
|
|
74
|
+
const cState = view.state.field(completionState, false);
|
|
75
|
+
if (
|
|
76
|
+
view.state.readOnly ||
|
|
77
|
+
!cState ||
|
|
78
|
+
!cState.open ||
|
|
79
|
+
cState.open.selected < 0 ||
|
|
80
|
+
Date.now() - cState.open.timestamp <
|
|
81
|
+
view.state.facet(completionConfig).interactionDelay
|
|
82
|
+
) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
applyCompletion(view, cState.open.options[cState.open.selected]);
|
|
86
|
+
return true;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/// Explicitly start autocompletion.
|
|
90
|
+
export const startCompletion: Command = (view: EditorView) => {
|
|
91
|
+
const cState = view.state.field(completionState, false);
|
|
92
|
+
if (!cState) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
view.dispatch({ effects: startCompletionEffect.of(true) });
|
|
96
|
+
return true;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/// Close the currently active completion.
|
|
100
|
+
export const closeCompletion: Command = (view: EditorView) => {
|
|
101
|
+
const cState = view.state.field(completionState, false);
|
|
102
|
+
if (!cState || !cState.active.some((a) => a.state !== State.Inactive)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
view.dispatch({ effects: closeCompletionEffect.of(null) });
|
|
106
|
+
return true;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
class RunningQuery {
|
|
110
|
+
time = Date.now();
|
|
111
|
+
updates: Transaction[] = [];
|
|
112
|
+
// Note that 'undefined' means 'not done yet', whereas 'null' means
|
|
113
|
+
// 'query returned null'.
|
|
114
|
+
done: undefined | CompletionResult | null = undefined;
|
|
115
|
+
|
|
116
|
+
constructor(
|
|
117
|
+
readonly active: ActiveSource,
|
|
118
|
+
readonly context: CompletionContext,
|
|
119
|
+
) {}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const DebounceTime = 50,
|
|
123
|
+
MaxUpdateCount = 50,
|
|
124
|
+
MinAbortTime = 1000;
|
|
125
|
+
|
|
126
|
+
const enum CompositionState {
|
|
127
|
+
None,
|
|
128
|
+
Started,
|
|
129
|
+
Changed,
|
|
130
|
+
ChangedAndMoved,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const completionPlugin = ViewPlugin.fromClass(
|
|
134
|
+
class implements PluginValue {
|
|
135
|
+
debounceUpdate: NodeJS.Timeout | -1 = -1;
|
|
136
|
+
running: RunningQuery[] = [];
|
|
137
|
+
debounceAccept: NodeJS.Timeout | -1 = -1;
|
|
138
|
+
composing = CompositionState.None;
|
|
139
|
+
|
|
140
|
+
constructor(readonly view: EditorView) {
|
|
141
|
+
for (const active of view.state.field(completionState).active) {
|
|
142
|
+
if (active.state === State.Pending) {
|
|
143
|
+
this.startQuery(active);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
update(update: ViewUpdate) {
|
|
149
|
+
const cState = update.state.field(completionState);
|
|
150
|
+
if (
|
|
151
|
+
!update.selectionSet &&
|
|
152
|
+
!update.docChanged &&
|
|
153
|
+
update.startState.field(completionState) === cState
|
|
154
|
+
) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const doesReset = update.transactions.some((tr) => {
|
|
159
|
+
return (tr.selection || tr.docChanged) && !getUserEvent(tr);
|
|
160
|
+
});
|
|
161
|
+
for (let i = 0; i < this.running.length; i++) {
|
|
162
|
+
const query = this.running[i];
|
|
163
|
+
if (
|
|
164
|
+
doesReset ||
|
|
165
|
+
(query.updates.length + update.transactions.length > MaxUpdateCount &&
|
|
166
|
+
Date.now() - query.time > MinAbortTime)
|
|
167
|
+
) {
|
|
168
|
+
for (const handler of query.context.abortListeners!) {
|
|
169
|
+
try {
|
|
170
|
+
handler();
|
|
171
|
+
} catch (e) {
|
|
172
|
+
logException(this.view.state, e);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
query.context.abortListeners = null;
|
|
176
|
+
this.running.splice(i--, 1);
|
|
177
|
+
} else {
|
|
178
|
+
query.updates.push(...update.transactions);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (this.debounceUpdate > -1) {
|
|
183
|
+
clearTimeout(this.debounceUpdate);
|
|
184
|
+
}
|
|
185
|
+
this.debounceUpdate = cState.active.some(
|
|
186
|
+
(a) =>
|
|
187
|
+
a.state === State.Pending &&
|
|
188
|
+
!this.running.some((q) => q.active.source === a.source),
|
|
189
|
+
)
|
|
190
|
+
? setTimeout(() => this.startUpdate(), DebounceTime)
|
|
191
|
+
: -1;
|
|
192
|
+
|
|
193
|
+
if (this.composing !== CompositionState.None) {
|
|
194
|
+
for (const tr of update.transactions) {
|
|
195
|
+
if (getUserEvent(tr) === 'input') {
|
|
196
|
+
this.composing = CompositionState.Changed;
|
|
197
|
+
} else if (this.composing === CompositionState.Changed && tr.selection) {
|
|
198
|
+
this.composing = CompositionState.ChangedAndMoved;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
startUpdate() {
|
|
205
|
+
this.debounceUpdate = -1;
|
|
206
|
+
const { state } = this.view,
|
|
207
|
+
cState = state.field(completionState);
|
|
208
|
+
for (const active of cState.active) {
|
|
209
|
+
if (
|
|
210
|
+
active.state === State.Pending &&
|
|
211
|
+
!this.running.some((r) => r.active.source === active.source)
|
|
212
|
+
) {
|
|
213
|
+
this.startQuery(active);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
startQuery(active: ActiveSource) {
|
|
219
|
+
const { state } = this.view,
|
|
220
|
+
pos = cur(state);
|
|
221
|
+
const context = new CompletionContext(state, pos, active.explicitPos === pos);
|
|
222
|
+
const pending = new RunningQuery(active, context);
|
|
223
|
+
this.running.push(pending);
|
|
224
|
+
Promise.resolve(active.source(context))
|
|
225
|
+
.then(
|
|
226
|
+
(result) => {
|
|
227
|
+
if (!pending.context.aborted) {
|
|
228
|
+
pending.done = result || null;
|
|
229
|
+
return this.scheduleAccept();
|
|
230
|
+
}
|
|
231
|
+
return undefined;
|
|
232
|
+
},
|
|
233
|
+
(err) => {
|
|
234
|
+
this.view.dispatch({ effects: closeCompletionEffect.of(null) });
|
|
235
|
+
logException(this.view.state, err);
|
|
236
|
+
return undefined;
|
|
237
|
+
},
|
|
238
|
+
)
|
|
239
|
+
.catch(() => {
|
|
240
|
+
//
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
scheduleAccept() {
|
|
245
|
+
if (this.running.every((q) => q.done !== undefined)) {
|
|
246
|
+
this.accept();
|
|
247
|
+
} else if (this.debounceAccept < 0) {
|
|
248
|
+
this.debounceAccept = setTimeout(() => this.accept(), DebounceTime);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// For each finished query in this.running, try to create a result
|
|
253
|
+
// or, if appropriate, restart the query.
|
|
254
|
+
accept() {
|
|
255
|
+
if (this.debounceAccept > -1) {
|
|
256
|
+
clearTimeout(this.debounceAccept);
|
|
257
|
+
}
|
|
258
|
+
this.debounceAccept = -1;
|
|
259
|
+
|
|
260
|
+
const updated: ActiveSource[] = [];
|
|
261
|
+
const conf = this.view.state.facet(completionConfig);
|
|
262
|
+
for (let i = 0; i < this.running.length; i++) {
|
|
263
|
+
const query = this.running[i];
|
|
264
|
+
if (query.done === undefined) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
this.running.splice(i--, 1);
|
|
268
|
+
|
|
269
|
+
if (query.done) {
|
|
270
|
+
let active: ActiveSource = new ActiveResult(
|
|
271
|
+
query.active.source,
|
|
272
|
+
query.active.explicitPos,
|
|
273
|
+
query.done,
|
|
274
|
+
query.done.from,
|
|
275
|
+
query.done.to ??
|
|
276
|
+
cur(query.updates.length ? query.updates[0].startState : this.view.state),
|
|
277
|
+
);
|
|
278
|
+
// Replay the transactions that happened since the start of
|
|
279
|
+
// the request and see if that preserves the result
|
|
280
|
+
for (const tr of query.updates) {
|
|
281
|
+
active = active.update(tr, conf);
|
|
282
|
+
}
|
|
283
|
+
if (active.hasResult()) {
|
|
284
|
+
updated.push(active);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const current = this.view.state
|
|
290
|
+
.field(completionState)
|
|
291
|
+
.active.find((a) => a.source === query.active.source);
|
|
292
|
+
if (current && current.state === State.Pending) {
|
|
293
|
+
if (query.done === null) {
|
|
294
|
+
// Explicitly failed. Should clear the pending status if it
|
|
295
|
+
// hasn't been re-set in the meantime.
|
|
296
|
+
let active = new ActiveSource(query.active.source, State.Inactive);
|
|
297
|
+
for (const tr of query.updates) {
|
|
298
|
+
active = active.update(tr, conf);
|
|
299
|
+
}
|
|
300
|
+
if (active.state !== State.Pending) {
|
|
301
|
+
updated.push(active);
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
// Cleared by subsequent transactions. Restart.
|
|
305
|
+
this.startQuery(current);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (updated.length) {
|
|
311
|
+
this.view.dispatch({ effects: setActiveEffect.of(updated) });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
eventHandlers: {
|
|
317
|
+
blur() {
|
|
318
|
+
const state = this.view.state.field(completionState, false);
|
|
319
|
+
if (
|
|
320
|
+
state &&
|
|
321
|
+
state.tooltip &&
|
|
322
|
+
this.view.state.facet(completionConfig).closeOnBlur
|
|
323
|
+
) {
|
|
324
|
+
this.view.dispatch({ effects: closeCompletionEffect.of(null) });
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
compositionstart() {
|
|
328
|
+
this.composing = CompositionState.Started;
|
|
329
|
+
},
|
|
330
|
+
compositionend() {
|
|
331
|
+
if (this.composing === CompositionState.ChangedAndMoved) {
|
|
332
|
+
// Safari fires compositionend events synchronously, possibly
|
|
333
|
+
// from inside an update, so dispatch asynchronously to avoid reentrancy
|
|
334
|
+
setTimeout(
|
|
335
|
+
() => this.view.dispatch({ effects: startCompletionEffect.of(false) }),
|
|
336
|
+
20,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
this.composing = CompositionState.None;
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/* eslint-disable prefer-const */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-shadow */
|
|
3
|
+
import type { Text } from '@codemirror/state';
|
|
4
|
+
|
|
5
|
+
import type { Completion, CompletionSource } from './completion.js';
|
|
6
|
+
|
|
7
|
+
const enum C {
|
|
8
|
+
Range = 50000,
|
|
9
|
+
MinCacheLen = 1000,
|
|
10
|
+
MaxList = 2000,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function wordRE(wordChars: string) {
|
|
14
|
+
const escaped = wordChars.replace(/[\\[.+*?(){|^$]/g, '\\$&');
|
|
15
|
+
try {
|
|
16
|
+
return new RegExp(`[\\p{Alphabetic}\\p{Number}_${escaped}]+`, 'ug');
|
|
17
|
+
} catch {
|
|
18
|
+
// eslint-disable-next-line no-useless-escape
|
|
19
|
+
return new RegExp(`[\w${escaped}]`, 'g');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function mapRE(re: RegExp, f: (source: string) => string) {
|
|
24
|
+
return new RegExp(f(re.source), re.unicode ? 'u' : '');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const wordCaches: Record<string, WeakMap<Text, readonly Completion[]>> = Object.create(
|
|
28
|
+
null,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
function wordCache(wordChars: string) {
|
|
32
|
+
return wordCaches[wordChars] || (wordCaches[wordChars] = new WeakMap());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function storeWords(
|
|
36
|
+
doc: Text,
|
|
37
|
+
wordRE: RegExp,
|
|
38
|
+
result: Completion[],
|
|
39
|
+
seen: Record<string, boolean>,
|
|
40
|
+
ignoreAt: number,
|
|
41
|
+
) {
|
|
42
|
+
for (let lines = doc.iterLines(), pos = 0; !lines.next().done; ) {
|
|
43
|
+
let { value } = lines,
|
|
44
|
+
m;
|
|
45
|
+
wordRE.lastIndex = 0;
|
|
46
|
+
while ((m = wordRE.exec(value))) {
|
|
47
|
+
if (!seen[m[0]] && pos + m.index !== ignoreAt) {
|
|
48
|
+
result.push({ type: 'text', label: m[0] });
|
|
49
|
+
seen[m[0]] = true;
|
|
50
|
+
if (result.length >= C.MaxList) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
pos += value.length + 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function collectWords(
|
|
60
|
+
doc: Text,
|
|
61
|
+
cache: WeakMap<Text, readonly Completion[]>,
|
|
62
|
+
wordRE: RegExp,
|
|
63
|
+
to: number,
|
|
64
|
+
ignoreAt: number,
|
|
65
|
+
) {
|
|
66
|
+
const big = doc.length >= C.MinCacheLen;
|
|
67
|
+
const cached = big && cache.get(doc);
|
|
68
|
+
if (cached) {
|
|
69
|
+
return cached;
|
|
70
|
+
}
|
|
71
|
+
const result: Completion[] = [],
|
|
72
|
+
seen: Record<string, boolean> = Object.create(null);
|
|
73
|
+
if (doc.children) {
|
|
74
|
+
let pos = 0;
|
|
75
|
+
for (const ch of doc.children) {
|
|
76
|
+
if (ch.length >= C.MinCacheLen) {
|
|
77
|
+
for (const c of collectWords(ch, cache, wordRE, to - pos, ignoreAt - pos)) {
|
|
78
|
+
if (!seen[c.label]) {
|
|
79
|
+
seen[c.label] = true;
|
|
80
|
+
result.push(c);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
storeWords(ch, wordRE, result, seen, ignoreAt - pos);
|
|
85
|
+
}
|
|
86
|
+
pos += ch.length + 1;
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
storeWords(doc, wordRE, result, seen, ignoreAt);
|
|
90
|
+
}
|
|
91
|
+
if (big && result.length < C.MaxList) {
|
|
92
|
+
cache.set(doc, result);
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// A completion source that will scan the document for words (using a
|
|
98
|
+
/// [character categorizer](#state.EditorState.charCategorizer)), and
|
|
99
|
+
/// return those as completions.
|
|
100
|
+
export const completeAnyWord: CompletionSource = (context) => {
|
|
101
|
+
const wordChars = context.state
|
|
102
|
+
.languageDataAt<string>('wordChars', context.pos)
|
|
103
|
+
.join('');
|
|
104
|
+
const re = wordRE(wordChars);
|
|
105
|
+
const token = context.matchBefore(mapRE(re, (s) => s + '$'));
|
|
106
|
+
if (!token && !context.explicit) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const from = token ? token.from : context.pos;
|
|
110
|
+
const options = collectWords(
|
|
111
|
+
context.state.doc,
|
|
112
|
+
wordCache(wordChars),
|
|
113
|
+
re,
|
|
114
|
+
C.Range,
|
|
115
|
+
from,
|
|
116
|
+
);
|
|
117
|
+
return { from, options, validFor: mapRE(re, (s) => '^' + s) };
|
|
118
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { CompletionSource, Completion } from '@codemirror/autocomplete';
|
|
2
|
+
import type { CompletionProvider, CompletionReply } from '@difizen/libro-code-editor';
|
|
3
|
+
import type { JSONObject } from '@difizen/libro-common';
|
|
4
|
+
|
|
5
|
+
type EditorCompletion = (provider: CompletionProvider | undefined) => CompletionSource;
|
|
6
|
+
|
|
7
|
+
export const kernelCompletions: EditorCompletion =
|
|
8
|
+
(provider: CompletionProvider | undefined) => async (context) => {
|
|
9
|
+
/**
|
|
10
|
+
* 只在显式的使用tab触发时调用kernel completion
|
|
11
|
+
* 只在只在隐式的输入时触发时调用lsp completion
|
|
12
|
+
*/
|
|
13
|
+
if (!provider || !context.explicit) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const word = context.matchBefore(/\w*/);
|
|
17
|
+
let result: CompletionReply;
|
|
18
|
+
const timeout = 500;
|
|
19
|
+
try {
|
|
20
|
+
result = await Promise.any([
|
|
21
|
+
provider({ cursorPosition: context.pos }),
|
|
22
|
+
new Promise<CompletionReply>((_resolve, reject) => {
|
|
23
|
+
setTimeout(() => {
|
|
24
|
+
reject(`request time out in ${timeout}ms`);
|
|
25
|
+
}, timeout);
|
|
26
|
+
}),
|
|
27
|
+
]);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error('provider error', error);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!word) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
if (word.from === word.to) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const metadata = result.metadata || {};
|
|
41
|
+
const types = metadata['_jupyter_types_experimental'] as JSONObject[];
|
|
42
|
+
let items: Completion[];
|
|
43
|
+
if (types) {
|
|
44
|
+
items = types.map((item) => {
|
|
45
|
+
return {
|
|
46
|
+
label: item['text'] as string,
|
|
47
|
+
type: item['type'] === '<unknown>' ? undefined : (item['type'] as string),
|
|
48
|
+
} as Completion;
|
|
49
|
+
});
|
|
50
|
+
} else {
|
|
51
|
+
items = result.matches.map((item) => {
|
|
52
|
+
return { label: item, type: 'text' };
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
from: result.cursor_start ?? word.from,
|
|
58
|
+
to: result.cursor_end,
|
|
59
|
+
options: items,
|
|
60
|
+
};
|
|
61
|
+
};
|