@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.
Files changed (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +0 -0
  3. package/es/auto-complete/closebrackets.d.ts +12 -0
  4. package/es/auto-complete/closebrackets.d.ts.map +1 -0
  5. package/es/auto-complete/closebrackets.js +408 -0
  6. package/es/auto-complete/completion.d.ts +57 -0
  7. package/es/auto-complete/completion.d.ts.map +1 -0
  8. package/es/auto-complete/completion.js +265 -0
  9. package/es/auto-complete/config.d.ts +22 -0
  10. package/es/auto-complete/config.d.ts.map +1 -0
  11. package/es/auto-complete/config.js +44 -0
  12. package/es/auto-complete/filter.d.ts +13 -0
  13. package/es/auto-complete/filter.d.ts.map +1 -0
  14. package/es/auto-complete/filter.js +191 -0
  15. package/es/auto-complete/index.d.ts +17 -0
  16. package/es/auto-complete/index.d.ts.map +1 -0
  17. package/es/auto-complete/index.js +107 -0
  18. package/es/auto-complete/snippet.d.ts +14 -0
  19. package/es/auto-complete/snippet.d.ts.map +1 -0
  20. package/es/auto-complete/snippet.js +447 -0
  21. package/es/auto-complete/state.d.ts +63 -0
  22. package/es/auto-complete/state.d.ts.map +1 -0
  23. package/es/auto-complete/state.js +452 -0
  24. package/es/auto-complete/theme.d.ts +6 -0
  25. package/es/auto-complete/theme.d.ts.map +1 -0
  26. package/es/auto-complete/theme.js +151 -0
  27. package/es/auto-complete/tooltip.d.ts +5 -0
  28. package/es/auto-complete/tooltip.d.ts.map +1 -0
  29. package/es/auto-complete/tooltip.js +365 -0
  30. package/es/auto-complete/view.d.ts +43 -0
  31. package/es/auto-complete/view.d.ts.map +1 -0
  32. package/es/auto-complete/view.js +372 -0
  33. package/es/auto-complete/word.d.ts +3 -0
  34. package/es/auto-complete/word.d.ts.map +1 -0
  35. package/es/auto-complete/word.js +119 -0
  36. package/es/completion.d.ts +6 -0
  37. package/es/completion.d.ts.map +1 -0
  38. package/es/completion.js +84 -0
  39. package/es/config.d.ts +184 -0
  40. package/es/config.d.ts.map +1 -0
  41. package/es/config.js +473 -0
  42. package/es/editor.d.ts +361 -0
  43. package/es/editor.d.ts.map +1 -0
  44. package/es/editor.js +1126 -0
  45. package/es/factory.d.ts +3 -0
  46. package/es/factory.d.ts.map +1 -0
  47. package/es/factory.js +12 -0
  48. package/es/hyperlink.d.ts +15 -0
  49. package/es/hyperlink.d.ts.map +1 -0
  50. package/es/hyperlink.js +120 -0
  51. package/es/indent.d.ts +8 -0
  52. package/es/indent.d.ts.map +1 -0
  53. package/es/indent.js +58 -0
  54. package/es/indentation-markers/config.d.ts +17 -0
  55. package/es/indentation-markers/config.d.ts.map +1 -0
  56. package/es/indentation-markers/config.js +10 -0
  57. package/es/indentation-markers/index.d.ts +3 -0
  58. package/es/indentation-markers/index.d.ts.map +1 -0
  59. package/es/indentation-markers/index.js +160 -0
  60. package/es/indentation-markers/map.d.ts +77 -0
  61. package/es/indentation-markers/map.d.ts.map +1 -0
  62. package/es/indentation-markers/map.js +265 -0
  63. package/es/indentation-markers/utils.d.ts +27 -0
  64. package/es/indentation-markers/utils.d.ts.map +1 -0
  65. package/es/indentation-markers/utils.js +91 -0
  66. package/es/index.d.ts +11 -0
  67. package/es/index.d.ts.map +1 -0
  68. package/es/index.js +10 -0
  69. package/es/libro-icon.d.ts +3 -0
  70. package/es/libro-icon.d.ts.map +1 -0
  71. package/es/libro-icon.js +2 -0
  72. package/es/mimetype.d.ts +22 -0
  73. package/es/mimetype.d.ts.map +1 -0
  74. package/es/mimetype.js +59 -0
  75. package/es/mode.d.ts +86 -0
  76. package/es/mode.d.ts.map +1 -0
  77. package/es/mode.js +284 -0
  78. package/es/monitor.d.ts +32 -0
  79. package/es/monitor.d.ts.map +1 -0
  80. package/es/monitor.js +129 -0
  81. package/es/python-lang.d.ts +3 -0
  82. package/es/python-lang.d.ts.map +1 -0
  83. package/es/python-lang.js +7 -0
  84. package/es/style/base.css +131 -0
  85. package/es/style/theme.css +12 -0
  86. package/es/style/variables.css +403 -0
  87. package/es/theme.d.ts +35 -0
  88. package/es/theme.d.ts.map +1 -0
  89. package/es/theme.js +225 -0
  90. package/es/tooltip.d.ts +10 -0
  91. package/es/tooltip.d.ts.map +1 -0
  92. package/es/tooltip.js +170 -0
  93. package/package.json +74 -0
  94. package/src/auto-complete/README.md +71 -0
  95. package/src/auto-complete/closebrackets.ts +423 -0
  96. package/src/auto-complete/completion.ts +345 -0
  97. package/src/auto-complete/config.ts +101 -0
  98. package/src/auto-complete/filter.ts +215 -0
  99. package/src/auto-complete/index.ts +112 -0
  100. package/src/auto-complete/snippet.ts +394 -0
  101. package/src/auto-complete/state.ts +472 -0
  102. package/src/auto-complete/theme.ts +126 -0
  103. package/src/auto-complete/tooltip.ts +386 -0
  104. package/src/auto-complete/view.ts +343 -0
  105. package/src/auto-complete/word.ts +118 -0
  106. package/src/completion.ts +61 -0
  107. package/src/config.ts +689 -0
  108. package/src/editor.ts +1078 -0
  109. package/src/factory.ts +10 -0
  110. package/src/hyperlink.ts +95 -0
  111. package/src/indent.ts +69 -0
  112. package/src/indentation-markers/config.ts +31 -0
  113. package/src/indentation-markers/index.ts +192 -0
  114. package/src/indentation-markers/map.ts +273 -0
  115. package/src/indentation-markers/utils.ts +84 -0
  116. package/src/index.ts +11 -0
  117. package/src/libro-icon.ts +4 -0
  118. package/src/mimetype.ts +49 -0
  119. package/src/mode.ts +269 -0
  120. package/src/monitor.ts +105 -0
  121. package/src/python-lang.ts +7 -0
  122. package/src/style/base.css +129 -0
  123. package/src/style/theme.css +12 -0
  124. package/src/style/variables.css +405 -0
  125. package/src/theme.ts +231 -0
  126. 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
+ }