@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,345 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-parameter-properties */
|
|
2
|
+
/* eslint-disable @typescript-eslint/parameter-properties */
|
|
3
|
+
import { syntaxTree } from '@codemirror/language';
|
|
4
|
+
import type { EditorState, TransactionSpec } from '@codemirror/state';
|
|
5
|
+
import { Annotation, EditorSelection } from '@codemirror/state';
|
|
6
|
+
import type { EditorView } from '@codemirror/view';
|
|
7
|
+
import type { SyntaxNode } from '@lezer/common';
|
|
8
|
+
|
|
9
|
+
import type { ActiveResult } from './state.js';
|
|
10
|
+
|
|
11
|
+
/// Objects type used to represent individual completions.
|
|
12
|
+
export interface Completion {
|
|
13
|
+
/// The label to show in the completion picker. This is what input
|
|
14
|
+
/// is matched agains to determine whether a completion matches (and
|
|
15
|
+
/// how well it matches).
|
|
16
|
+
label: string;
|
|
17
|
+
/// An optional short piece of information to show (with a different
|
|
18
|
+
/// style) after the label.
|
|
19
|
+
detail?: string;
|
|
20
|
+
/// Additional info to show when the completion is selected. Can be
|
|
21
|
+
/// a plain string or a function that'll render the DOM structure to
|
|
22
|
+
/// show when invoked.
|
|
23
|
+
info?: string | ((completion: Completion) => Node | null | Promise<Node | null>);
|
|
24
|
+
/// How to apply the completion. The default is to replace it with
|
|
25
|
+
/// its [label](#autocomplete.Completion.label). When this holds a
|
|
26
|
+
/// string, the completion range is replaced by that string. When it
|
|
27
|
+
/// is a function, that function is called to perform the
|
|
28
|
+
/// completion. If it fires a transaction, it is responsible for
|
|
29
|
+
/// adding the [`pickedCompletion`](#autocomplete.pickedCompletion)
|
|
30
|
+
/// annotation to it.
|
|
31
|
+
apply?:
|
|
32
|
+
| string
|
|
33
|
+
| ((view: EditorView, completion: Completion, from: number, to: number) => void);
|
|
34
|
+
/// The type of the completion. This is used to pick an icon to show
|
|
35
|
+
/// for the completion. Icons are styled with a CSS class created by
|
|
36
|
+
/// appending the type name to `"cm-completionIcon-"`. You can
|
|
37
|
+
/// define or restyle icons by defining these selectors. The base
|
|
38
|
+
/// library defines simple icons for `class`, `constant`, `enum`,
|
|
39
|
+
/// `function`, `interface`, `keyword`, `method`, `namespace`,
|
|
40
|
+
/// `property`, `text`, `type`, and `variable`.
|
|
41
|
+
///
|
|
42
|
+
/// Multiple types can be provided by separating them with spaces.
|
|
43
|
+
type?: string;
|
|
44
|
+
/// When given, should be a number from -99 to 99 that adjusts how
|
|
45
|
+
/// this completion is ranked compared to other completions that
|
|
46
|
+
/// match the input as well as this one. A negative number moves it
|
|
47
|
+
/// down the list, a positive number moves it up.
|
|
48
|
+
boost?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// An instance of this is passed to completion source functions.
|
|
52
|
+
export class CompletionContext {
|
|
53
|
+
/// @internal
|
|
54
|
+
abortListeners: (() => void)[] | null = [];
|
|
55
|
+
|
|
56
|
+
/// Create a new completion context. (Mostly useful for testing
|
|
57
|
+
/// completion sources—in the editor, the extension will create
|
|
58
|
+
/// these for you.)
|
|
59
|
+
constructor(
|
|
60
|
+
/// The editor state that the completion happens in.
|
|
61
|
+
readonly state: EditorState,
|
|
62
|
+
/// The position at which the completion is happening.
|
|
63
|
+
readonly pos: number,
|
|
64
|
+
/// Indicates whether completion was activated explicitly, or
|
|
65
|
+
/// implicitly by typing. The usual way to respond to this is to
|
|
66
|
+
/// only return completions when either there is part of a
|
|
67
|
+
/// completable entity before the cursor, or `explicit` is true.
|
|
68
|
+
readonly explicit: boolean,
|
|
69
|
+
) {}
|
|
70
|
+
|
|
71
|
+
/// Get the extent, content, and (if there is a token) type of the
|
|
72
|
+
/// token before `this.pos`.
|
|
73
|
+
tokenBefore(types: readonly string[]) {
|
|
74
|
+
let token: SyntaxNode | null = syntaxTree(this.state).resolveInner(this.pos, -1);
|
|
75
|
+
while (token && types.indexOf(token.name) < 0) {
|
|
76
|
+
token = token.parent;
|
|
77
|
+
}
|
|
78
|
+
return token
|
|
79
|
+
? {
|
|
80
|
+
from: token.from,
|
|
81
|
+
to: this.pos,
|
|
82
|
+
text: this.state.sliceDoc(token.from, this.pos),
|
|
83
|
+
type: token.type,
|
|
84
|
+
}
|
|
85
|
+
: null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Get the match of the given expression directly before the
|
|
89
|
+
/// cursor.
|
|
90
|
+
matchBefore(expr: RegExp) {
|
|
91
|
+
const line = this.state.doc.lineAt(this.pos);
|
|
92
|
+
const start = Math.max(line.from, this.pos - 250);
|
|
93
|
+
const str = line.text.slice(start - line.from, this.pos - line.from);
|
|
94
|
+
const found = str.search(ensureAnchor(expr, false));
|
|
95
|
+
return found < 0
|
|
96
|
+
? null
|
|
97
|
+
: { from: start + found, to: this.pos, text: str.slice(found) };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Yields true when the query has been aborted. Can be useful in
|
|
101
|
+
/// asynchronous queries to avoid doing work that will be ignored.
|
|
102
|
+
get aborted() {
|
|
103
|
+
return this.abortListeners === null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Allows you to register abort handlers, which will be called when
|
|
107
|
+
/// the query is
|
|
108
|
+
/// [aborted](#autocomplete.CompletionContext.aborted).
|
|
109
|
+
addEventListener(type: 'abort', listener: () => void) {
|
|
110
|
+
if (type === 'abort' && this.abortListeners) {
|
|
111
|
+
this.abortListeners.push(listener);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function toSet(chars: Record<string, true>) {
|
|
117
|
+
let flat = Object.keys(chars).join('');
|
|
118
|
+
const words = /\w/.test(flat);
|
|
119
|
+
if (words) {
|
|
120
|
+
flat = flat.replace(/\w/g, '');
|
|
121
|
+
}
|
|
122
|
+
return `[${words ? '\\w' : ''}${flat.replace(/[^\w\s]/g, '\\$&')}]`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function prefixMatch(options: readonly Completion[]) {
|
|
126
|
+
const first = Object.create(null),
|
|
127
|
+
rest = Object.create(null);
|
|
128
|
+
for (const { label } of options) {
|
|
129
|
+
first[label[0]] = true;
|
|
130
|
+
for (let i = 1; i < label.length; i++) {
|
|
131
|
+
rest[label[i]] = true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const source = toSet(first) + toSet(rest) + '*$';
|
|
135
|
+
return [new RegExp('^' + source), new RegExp(source)];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// Given a a fixed array of options, return an autocompleter that
|
|
139
|
+
/// completes them.
|
|
140
|
+
export function completeFromList(
|
|
141
|
+
list: readonly (string | Completion)[],
|
|
142
|
+
): CompletionSource {
|
|
143
|
+
const options = list.map((o) => (typeof o === 'string' ? { label: o } : o));
|
|
144
|
+
const [validFor, match] = options.every((o) => /^\w+$/.test(o.label))
|
|
145
|
+
? [/\w*$/, /\w+$/]
|
|
146
|
+
: prefixMatch(options);
|
|
147
|
+
return (context: CompletionContext) => {
|
|
148
|
+
const token = context.matchBefore(match);
|
|
149
|
+
return token || context.explicit
|
|
150
|
+
? { from: token ? token.from : context.pos, options, validFor }
|
|
151
|
+
: null;
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// Wrap the given completion source so that it will only fire when the
|
|
156
|
+
/// cursor is in a syntax node with one of the given names.
|
|
157
|
+
export function ifIn(
|
|
158
|
+
nodes: readonly string[],
|
|
159
|
+
source: CompletionSource,
|
|
160
|
+
): CompletionSource {
|
|
161
|
+
return (context: CompletionContext) => {
|
|
162
|
+
for (
|
|
163
|
+
let pos: SyntaxNode | null = syntaxTree(context.state).resolveInner(
|
|
164
|
+
context.pos,
|
|
165
|
+
-1,
|
|
166
|
+
);
|
|
167
|
+
pos;
|
|
168
|
+
pos = pos.parent
|
|
169
|
+
) {
|
|
170
|
+
if (nodes.indexOf(pos.name) > -1) {
|
|
171
|
+
return source(context);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// Wrap the given completion source so that it will not fire when the
|
|
179
|
+
/// cursor is in a syntax node with one of the given names.
|
|
180
|
+
export function ifNotIn(
|
|
181
|
+
nodes: readonly string[],
|
|
182
|
+
source: CompletionSource,
|
|
183
|
+
): CompletionSource {
|
|
184
|
+
return (context: CompletionContext) => {
|
|
185
|
+
for (
|
|
186
|
+
let pos: SyntaxNode | null = syntaxTree(context.state).resolveInner(
|
|
187
|
+
context.pos,
|
|
188
|
+
-1,
|
|
189
|
+
);
|
|
190
|
+
pos;
|
|
191
|
+
pos = pos.parent
|
|
192
|
+
) {
|
|
193
|
+
if (nodes.indexOf(pos.name) > -1) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return source(context);
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// The function signature for a completion source. Such a function
|
|
202
|
+
/// may return its [result](#autocomplete.CompletionResult)
|
|
203
|
+
/// synchronously or as a promise. Returning null indicates no
|
|
204
|
+
/// completions are available.
|
|
205
|
+
export type CompletionSource = (
|
|
206
|
+
context: CompletionContext,
|
|
207
|
+
) => CompletionResult | null | Promise<CompletionResult | null>;
|
|
208
|
+
|
|
209
|
+
/// Interface for objects returned by completion sources.
|
|
210
|
+
export interface CompletionResult {
|
|
211
|
+
/// The start of the range that is being completed.
|
|
212
|
+
from: number;
|
|
213
|
+
/// The end of the range that is being completed. Defaults to the
|
|
214
|
+
/// main cursor position.
|
|
215
|
+
to?: number;
|
|
216
|
+
/// The completions returned. These don't have to be compared with
|
|
217
|
+
/// the input by the source—the autocompletion system will do its
|
|
218
|
+
/// own matching (against the text between `from` and `to`) and
|
|
219
|
+
/// sorting.
|
|
220
|
+
options: readonly Completion[];
|
|
221
|
+
/// When given, further typing or deletion that causes the part of
|
|
222
|
+
/// the document between ([mapped](#state.ChangeDesc.mapPos)) `from`
|
|
223
|
+
/// and `to` to match this regular expression or predicate function
|
|
224
|
+
/// will not query the completion source again, but continue with
|
|
225
|
+
/// this list of options. This can help a lot with responsiveness,
|
|
226
|
+
/// since it allows the completion list to be updated synchronously.
|
|
227
|
+
validFor?:
|
|
228
|
+
| RegExp
|
|
229
|
+
| ((text: string, from: number, to: number, state: EditorState) => boolean);
|
|
230
|
+
/// By default, the library filters and scores completions. Set
|
|
231
|
+
/// `filter` to `false` to disable this, and cause your completions
|
|
232
|
+
/// to all be included, in the order they were given. When there are
|
|
233
|
+
/// other sources, unfiltered completions appear at the top of the
|
|
234
|
+
/// list of completions. `validFor` must not be given when `filter`
|
|
235
|
+
/// is `false`, because it only works when filtering.
|
|
236
|
+
filter?: boolean;
|
|
237
|
+
/// When [`filter`](#autocomplete.CompletionResult.filter) is set to
|
|
238
|
+
/// `false`, this may be provided to compute the ranges on the label
|
|
239
|
+
/// that match the input. Should return an array of numbers where
|
|
240
|
+
/// each pair of adjacent numbers provide the start and end of a
|
|
241
|
+
/// range.
|
|
242
|
+
getMatch?: (completion: Completion) => readonly number[];
|
|
243
|
+
/// Synchronously update the completion result after typing or
|
|
244
|
+
/// deletion. If given, this should not do any expensive work, since
|
|
245
|
+
/// it will be called during editor state updates. The function
|
|
246
|
+
/// should make sure (similar to
|
|
247
|
+
/// [`validFor`](#autocomplete.CompletionResult.validFor)) that the
|
|
248
|
+
/// completion still applies in the new state.
|
|
249
|
+
update?: (
|
|
250
|
+
current: CompletionResult,
|
|
251
|
+
from: number,
|
|
252
|
+
to: number,
|
|
253
|
+
context: CompletionContext,
|
|
254
|
+
) => CompletionResult | null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export class Option {
|
|
258
|
+
constructor(
|
|
259
|
+
readonly completion: Completion,
|
|
260
|
+
readonly source: ActiveResult,
|
|
261
|
+
readonly match: readonly number[],
|
|
262
|
+
) {}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function cur(state: EditorState) {
|
|
266
|
+
return state.selection.main.head;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Make sure the given regexp has a $ at its end and, if `start` is
|
|
270
|
+
// true, a ^ at its start.
|
|
271
|
+
export function ensureAnchor(expr: RegExp, start: boolean) {
|
|
272
|
+
const { source } = expr;
|
|
273
|
+
const addStart = start && source[0] !== '^',
|
|
274
|
+
addEnd = source[source.length - 1] !== '$';
|
|
275
|
+
if (!addStart && !addEnd) {
|
|
276
|
+
return expr;
|
|
277
|
+
}
|
|
278
|
+
return new RegExp(
|
|
279
|
+
`${addStart ? '^' : ''}(?:${source})${addEnd ? '$' : ''}`,
|
|
280
|
+
expr.flags ?? (expr.ignoreCase ? 'i' : ''),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/// This annotation is added to transactions that are produced by
|
|
285
|
+
/// picking a completion.
|
|
286
|
+
export const pickedCompletion = Annotation.define<Completion>();
|
|
287
|
+
|
|
288
|
+
/// Helper function that returns a transaction spec which inserts a
|
|
289
|
+
/// completion's text in the main selection range, and any other
|
|
290
|
+
/// selection range that has the same text in front of it.
|
|
291
|
+
export function insertCompletionText(
|
|
292
|
+
state: EditorState,
|
|
293
|
+
text: string,
|
|
294
|
+
from: number,
|
|
295
|
+
to: number,
|
|
296
|
+
): TransactionSpec {
|
|
297
|
+
return {
|
|
298
|
+
...state.changeByRange((range) => {
|
|
299
|
+
if (range === state.selection.main) {
|
|
300
|
+
return {
|
|
301
|
+
changes: { from: from, to: to, insert: text },
|
|
302
|
+
range: EditorSelection.cursor(from + text.length),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const len = to - from;
|
|
306
|
+
if (
|
|
307
|
+
!range.empty ||
|
|
308
|
+
(len &&
|
|
309
|
+
state.sliceDoc(range.from - len, range.from) !== state.sliceDoc(from, to))
|
|
310
|
+
) {
|
|
311
|
+
return { range };
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
changes: { from: range.from - len, to: range.from, insert: text },
|
|
315
|
+
range: EditorSelection.cursor(range.from - len + text.length),
|
|
316
|
+
};
|
|
317
|
+
}),
|
|
318
|
+
userEvent: 'input.complete',
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function applyCompletion(view: EditorView, option: Option) {
|
|
323
|
+
const apply = option.completion.apply || option.completion.label;
|
|
324
|
+
const result = option.source;
|
|
325
|
+
if (typeof apply === 'string') {
|
|
326
|
+
view.dispatch(insertCompletionText(view.state, apply, result.from, result.to));
|
|
327
|
+
} else {
|
|
328
|
+
apply(view, option.completion, result.from, result.to);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const SourceCache = new WeakMap<readonly (string | Completion)[], CompletionSource>();
|
|
333
|
+
|
|
334
|
+
export function asSource(
|
|
335
|
+
source: CompletionSource | readonly (string | Completion)[],
|
|
336
|
+
): CompletionSource {
|
|
337
|
+
if (!Array.isArray(source)) {
|
|
338
|
+
return source as CompletionSource;
|
|
339
|
+
}
|
|
340
|
+
let known = SourceCache.get(source);
|
|
341
|
+
if (!known) {
|
|
342
|
+
SourceCache.set(source, (known = completeFromList(source)));
|
|
343
|
+
}
|
|
344
|
+
return known;
|
|
345
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { EditorState } from '@codemirror/state';
|
|
2
|
+
import { Facet, combineConfig } from '@codemirror/state';
|
|
3
|
+
|
|
4
|
+
import type { Completion, CompletionSource } from './completion.js';
|
|
5
|
+
|
|
6
|
+
export interface CompletionConfig {
|
|
7
|
+
/// When enabled (defaults to true), autocompletion will start
|
|
8
|
+
/// whenever the user types something that can be completed.
|
|
9
|
+
activateOnTyping?: boolean;
|
|
10
|
+
/// By default, when completion opens, the first option is selected
|
|
11
|
+
/// and can be confirmed with
|
|
12
|
+
/// [`acceptCompletion`](#autocomplete.acceptCompletion). When this
|
|
13
|
+
/// is set to false, the completion widget starts with no completion
|
|
14
|
+
/// selected, and the user has to explicitly move to a completion
|
|
15
|
+
/// before you can confirm one.
|
|
16
|
+
selectOnOpen?: boolean;
|
|
17
|
+
/// Override the completion sources used. By default, they will be
|
|
18
|
+
/// taken from the `"autocomplete"` [language
|
|
19
|
+
/// data](#state.EditorState.languageDataAt) (which should hold
|
|
20
|
+
/// [completion sources](#autocomplete.CompletionSource) or arrays
|
|
21
|
+
/// of [completions](#autocomplete.Completion)).
|
|
22
|
+
override?: readonly CompletionSource[] | null;
|
|
23
|
+
/// Determines whether the completion tooltip is closed when the
|
|
24
|
+
/// editor loses focus. Defaults to true.
|
|
25
|
+
closeOnBlur?: boolean;
|
|
26
|
+
/// The maximum number of options to render to the DOM.
|
|
27
|
+
maxRenderedOptions?: number;
|
|
28
|
+
/// Set this to false to disable the [default completion
|
|
29
|
+
/// keymap](#autocomplete.completionKeymap). (This requires you to
|
|
30
|
+
/// add bindings to control completion yourself. The bindings should
|
|
31
|
+
/// probably have a higher precedence than other bindings for the
|
|
32
|
+
/// same keys.)
|
|
33
|
+
defaultKeymap?: boolean;
|
|
34
|
+
/// By default, completions are shown below the cursor when there is
|
|
35
|
+
/// space. Setting this to true will make the extension put the
|
|
36
|
+
/// completions above the cursor when possible.
|
|
37
|
+
aboveCursor?: boolean;
|
|
38
|
+
/// This can be used to add additional CSS classes to completion
|
|
39
|
+
/// options.
|
|
40
|
+
optionClass?: (completion: Completion) => string;
|
|
41
|
+
/// By default, the library will render icons based on the
|
|
42
|
+
/// completion's [type](#autocomplete.Completion.type) in front of
|
|
43
|
+
/// each option. Set this to false to turn that off.
|
|
44
|
+
icons?: boolean;
|
|
45
|
+
/// This option can be used to inject additional content into
|
|
46
|
+
/// options. The `render` function will be called for each visible
|
|
47
|
+
/// completion, and should produce a DOM node to show. `position`
|
|
48
|
+
/// determines where in the DOM the result appears, relative to
|
|
49
|
+
/// other added widgets and the standard content. The default icons
|
|
50
|
+
/// have position 20, the label position 50, and the detail position
|
|
51
|
+
/// 80.
|
|
52
|
+
addToOptions?: {
|
|
53
|
+
render: (completion: Completion, state: EditorState) => Node | null;
|
|
54
|
+
position: number;
|
|
55
|
+
}[];
|
|
56
|
+
/// The comparison function to use when sorting completions with the same
|
|
57
|
+
/// match score. Defaults to using
|
|
58
|
+
/// [`localeCompare`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare).
|
|
59
|
+
compareCompletions?: (a: Completion, b: Completion) => number;
|
|
60
|
+
/// By default, commands relating to an open completion only take
|
|
61
|
+
/// effect 75 milliseconds after the completion opened, so that key
|
|
62
|
+
/// presses made before the user is aware of the tooltip don't go to
|
|
63
|
+
/// the tooltip. This option can be used to configure that delay.
|
|
64
|
+
interactionDelay?: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const completionConfig = Facet.define<
|
|
68
|
+
CompletionConfig,
|
|
69
|
+
Required<CompletionConfig>
|
|
70
|
+
>({
|
|
71
|
+
combine(configs) {
|
|
72
|
+
return combineConfig(
|
|
73
|
+
configs,
|
|
74
|
+
{
|
|
75
|
+
activateOnTyping: true,
|
|
76
|
+
selectOnOpen: true,
|
|
77
|
+
override: null,
|
|
78
|
+
closeOnBlur: true,
|
|
79
|
+
maxRenderedOptions: 100,
|
|
80
|
+
defaultKeymap: true,
|
|
81
|
+
optionClass: () => '',
|
|
82
|
+
aboveCursor: false,
|
|
83
|
+
icons: true,
|
|
84
|
+
addToOptions: [],
|
|
85
|
+
compareCompletions: (a, b) => a.label.localeCompare(b.label),
|
|
86
|
+
interactionDelay: 75,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
defaultKeymap: (a, b) => a && b,
|
|
90
|
+
closeOnBlur: (a, b) => a && b,
|
|
91
|
+
icons: (a, b) => a && b,
|
|
92
|
+
optionClass: (a, b) => (c) => joinClass(a(c), b(c)),
|
|
93
|
+
addToOptions: (a, b) => a.concat(b),
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
function joinClass(a: string, b: string) {
|
|
100
|
+
return a ? (b ? a + ' ' + b : a) : b;
|
|
101
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/* eslint-disable prefer-const */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-parameter-properties */
|
|
3
|
+
/* eslint-disable @typescript-eslint/parameter-properties */
|
|
4
|
+
import { codePointAt, codePointSize, fromCodePoint } from '@codemirror/state';
|
|
5
|
+
|
|
6
|
+
// Scores are counted from 0 (great match) down to negative numbers,
|
|
7
|
+
// assigning specific penalty values for specific shortcomings.
|
|
8
|
+
const enum Penalty {
|
|
9
|
+
Gap = -1100, // Added for each gap in the match (not counted for by-word matches)
|
|
10
|
+
NotStart = -700, // The match doesn't start at the start of the word
|
|
11
|
+
CaseFold = -200, // At least one character needed to be case-folded to match
|
|
12
|
+
ByWord = -100, // The match is by-word, meaning each char in the pattern matches the start of a word in the string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const enum Tp {
|
|
16
|
+
NonWord,
|
|
17
|
+
Upper,
|
|
18
|
+
Lower,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// A pattern matcher for fuzzy completion matching. Create an instance
|
|
22
|
+
// once for a pattern, and then use that to match any number of
|
|
23
|
+
// completions.
|
|
24
|
+
export class FuzzyMatcher {
|
|
25
|
+
chars: number[] = [];
|
|
26
|
+
folded: number[] = [];
|
|
27
|
+
astral: boolean;
|
|
28
|
+
|
|
29
|
+
// Buffers reused by calls to `match` to track matched character
|
|
30
|
+
// positions.
|
|
31
|
+
any: number[] = [];
|
|
32
|
+
precise: number[] = [];
|
|
33
|
+
byWord: number[] = [];
|
|
34
|
+
|
|
35
|
+
constructor(readonly pattern: string) {
|
|
36
|
+
for (let p = 0; p < pattern.length; ) {
|
|
37
|
+
const char = codePointAt(pattern, p),
|
|
38
|
+
size = codePointSize(char);
|
|
39
|
+
this.chars.push(char);
|
|
40
|
+
const part = pattern.slice(p, p + size),
|
|
41
|
+
upper = part.toUpperCase();
|
|
42
|
+
this.folded.push(codePointAt(upper === part ? part.toLowerCase() : upper, 0));
|
|
43
|
+
p += size;
|
|
44
|
+
}
|
|
45
|
+
this.astral = pattern.length !== this.chars.length;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Matches a given word (completion) against the pattern (input).
|
|
49
|
+
// Will return null for no match, and otherwise an array that starts
|
|
50
|
+
// with the match score, followed by any number of `from, to` pairs
|
|
51
|
+
// indicating the matched parts of `word`.
|
|
52
|
+
//
|
|
53
|
+
// The score is a number that is more negative the worse the match
|
|
54
|
+
// is. See `Penalty` above.
|
|
55
|
+
match(word: string): number[] | null {
|
|
56
|
+
if (this.pattern.length === 0) {
|
|
57
|
+
return [0];
|
|
58
|
+
}
|
|
59
|
+
if (word.length < this.pattern.length) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const { chars, folded, any, precise, byWord } = this;
|
|
63
|
+
// For single-character queries, only match when they occur right
|
|
64
|
+
// at the start
|
|
65
|
+
if (chars.length === 1) {
|
|
66
|
+
const first = codePointAt(word, 0);
|
|
67
|
+
return first === chars[0]
|
|
68
|
+
? [0, 0, codePointSize(first)]
|
|
69
|
+
: first === folded[0]
|
|
70
|
+
? [Penalty.CaseFold, 0, codePointSize(first)]
|
|
71
|
+
: null;
|
|
72
|
+
}
|
|
73
|
+
const direct = word.indexOf(this.pattern);
|
|
74
|
+
if (direct === 0) {
|
|
75
|
+
return [0, 0, this.pattern.length];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let len = chars.length,
|
|
79
|
+
anyTo = 0;
|
|
80
|
+
if (direct < 0) {
|
|
81
|
+
for (let i = 0, e = Math.min(word.length, 200); i < e && anyTo < len; ) {
|
|
82
|
+
const next = codePointAt(word, i);
|
|
83
|
+
if (next === chars[anyTo] || next === folded[anyTo]) {
|
|
84
|
+
any[anyTo++] = i;
|
|
85
|
+
}
|
|
86
|
+
i += codePointSize(next);
|
|
87
|
+
}
|
|
88
|
+
// No match, exit immediately
|
|
89
|
+
if (anyTo < len) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// This tracks the extent of the precise (non-folded, not
|
|
95
|
+
// necessarily adjacent) match
|
|
96
|
+
let preciseTo = 0;
|
|
97
|
+
// Tracks whether there is a match that hits only characters that
|
|
98
|
+
// appear to be starting words. `byWordFolded` is set to true when
|
|
99
|
+
// a case folded character is encountered in such a match
|
|
100
|
+
let byWordTo = 0,
|
|
101
|
+
byWordFolded = false;
|
|
102
|
+
// If we've found a partial adjacent match, these track its state
|
|
103
|
+
let adjacentTo = 0,
|
|
104
|
+
adjacentStart = -1,
|
|
105
|
+
adjacentEnd = -1;
|
|
106
|
+
let hasLower = /[a-z]/.test(word),
|
|
107
|
+
wordAdjacent = true;
|
|
108
|
+
// Go over the option's text, scanning for the various kinds of matches
|
|
109
|
+
for (
|
|
110
|
+
let i = 0, e = Math.min(word.length, 200), prevType = Tp.NonWord;
|
|
111
|
+
i < e && byWordTo < len;
|
|
112
|
+
|
|
113
|
+
) {
|
|
114
|
+
const next = codePointAt(word, i);
|
|
115
|
+
if (direct < 0) {
|
|
116
|
+
if (preciseTo < len && next === chars[preciseTo]) {
|
|
117
|
+
precise[preciseTo++] = i;
|
|
118
|
+
}
|
|
119
|
+
if (adjacentTo < len) {
|
|
120
|
+
if (next === chars[adjacentTo] || next === folded[adjacentTo]) {
|
|
121
|
+
if (adjacentTo === 0) {
|
|
122
|
+
adjacentStart = i;
|
|
123
|
+
}
|
|
124
|
+
adjacentEnd = i + 1;
|
|
125
|
+
adjacentTo++;
|
|
126
|
+
} else {
|
|
127
|
+
adjacentTo = 0;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
let ch,
|
|
132
|
+
type =
|
|
133
|
+
next < 0xff
|
|
134
|
+
? (next >= 48 && next <= 57) || (next >= 97 && next <= 122)
|
|
135
|
+
? Tp.Lower
|
|
136
|
+
: next >= 65 && next <= 90
|
|
137
|
+
? Tp.Upper
|
|
138
|
+
: Tp.NonWord
|
|
139
|
+
: (ch = fromCodePoint(next)) !== ch.toLowerCase()
|
|
140
|
+
? Tp.Upper
|
|
141
|
+
: ch !== ch.toUpperCase()
|
|
142
|
+
? Tp.Lower
|
|
143
|
+
: Tp.NonWord;
|
|
144
|
+
if (
|
|
145
|
+
!i ||
|
|
146
|
+
(type === Tp.Upper && hasLower) ||
|
|
147
|
+
(prevType === Tp.NonWord && type !== Tp.NonWord)
|
|
148
|
+
) {
|
|
149
|
+
if (
|
|
150
|
+
chars[byWordTo] === next ||
|
|
151
|
+
(folded[byWordTo] === next && (byWordFolded = true))
|
|
152
|
+
) {
|
|
153
|
+
byWord[byWordTo++] = i;
|
|
154
|
+
} else if (byWord.length) {
|
|
155
|
+
wordAdjacent = false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
prevType = type;
|
|
159
|
+
i += codePointSize(next);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (byWordTo === len && byWord[0] === 0 && wordAdjacent) {
|
|
163
|
+
return this.result(
|
|
164
|
+
Penalty.ByWord + (byWordFolded ? Penalty.CaseFold : 0),
|
|
165
|
+
byWord,
|
|
166
|
+
word,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (adjacentTo === len && adjacentStart === 0) {
|
|
170
|
+
return [Penalty.CaseFold - word.length, 0, adjacentEnd];
|
|
171
|
+
}
|
|
172
|
+
if (direct > -1) {
|
|
173
|
+
return [Penalty.NotStart - word.length, direct, direct + this.pattern.length];
|
|
174
|
+
}
|
|
175
|
+
if (adjacentTo === len) {
|
|
176
|
+
return [
|
|
177
|
+
Penalty.CaseFold + Penalty.NotStart - word.length,
|
|
178
|
+
adjacentStart,
|
|
179
|
+
adjacentEnd,
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
if (byWordTo === len) {
|
|
183
|
+
return this.result(
|
|
184
|
+
Penalty.ByWord +
|
|
185
|
+
(byWordFolded ? Penalty.CaseFold : 0) +
|
|
186
|
+
Penalty.NotStart +
|
|
187
|
+
(wordAdjacent ? 0 : Penalty.Gap),
|
|
188
|
+
byWord,
|
|
189
|
+
word,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return chars.length === 2
|
|
193
|
+
? null
|
|
194
|
+
: this.result(
|
|
195
|
+
(any[0] ? Penalty.NotStart : 0) + Penalty.CaseFold + Penalty.Gap,
|
|
196
|
+
any,
|
|
197
|
+
word,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
result(score: number, positions: number[], word: string) {
|
|
202
|
+
let result = [score - word.length],
|
|
203
|
+
i = 1;
|
|
204
|
+
for (const pos of positions) {
|
|
205
|
+
const to = pos + (this.astral ? codePointSize(codePointAt(word, pos)) : 1);
|
|
206
|
+
if (i > 1 && result[i - 1] === pos) {
|
|
207
|
+
result[i - 1] = to;
|
|
208
|
+
} else {
|
|
209
|
+
result[i++] = pos;
|
|
210
|
+
result[i++] = to;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
}
|