@opensumi/ide-utils 2.21.13 → 2.22.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/lib/arrays.d.ts +3 -3
- package/lib/arrays.d.ts.map +1 -1
- package/lib/arrays.js.map +1 -1
- package/lib/async.d.ts +3 -3
- package/lib/async.d.ts.map +1 -1
- package/lib/buffer.js +4 -4
- package/lib/buffer.js.map +1 -1
- package/lib/character-classifier.js +3 -3
- package/lib/character-classifier.js.map +1 -1
- package/lib/decorators.d.ts +1 -1
- package/lib/decorators.d.ts.map +1 -1
- package/lib/encoding.d.ts +7 -1
- package/lib/encoding.d.ts.map +1 -1
- package/lib/encoding.js +15 -2
- package/lib/encoding.js.map +1 -1
- package/lib/errors.d.ts +2 -2
- package/lib/errors.d.ts.map +1 -1
- package/lib/event.d.ts +2 -2
- package/lib/event.d.ts.map +1 -1
- package/lib/filters.d.ts +3 -3
- package/lib/filters.d.ts.map +1 -1
- package/lib/filters.js +25 -25
- package/lib/filters.js.map +1 -1
- package/lib/glob.d.ts +2 -2
- package/lib/glob.d.ts.map +1 -1
- package/lib/glob.js +1 -1
- package/lib/glob.js.map +1 -1
- package/lib/hash.js +10 -10
- package/lib/hash.js.map +1 -1
- package/lib/iterator.d.ts +1 -1
- package/lib/iterator.d.ts.map +1 -1
- package/lib/linked-text.d.ts +1 -1
- package/lib/linked-text.d.ts.map +1 -1
- package/lib/linked-text.js.map +1 -1
- package/lib/map.js +17 -17
- package/lib/map.js.map +1 -1
- package/lib/objects.d.ts +1 -0
- package/lib/objects.d.ts.map +1 -1
- package/lib/objects.js +38 -1
- package/lib/objects.js.map +1 -1
- package/lib/os.d.ts +1 -8
- package/lib/os.d.ts.map +1 -1
- package/lib/os.js +4 -8
- package/lib/os.js.map +1 -1
- package/lib/path.d.ts +1 -1
- package/lib/path.d.ts.map +1 -1
- package/lib/path.js +29 -29
- package/lib/path.js.map +1 -1
- package/lib/platform.js +8 -8
- package/lib/platform.js.map +1 -1
- package/lib/process.d.ts.map +1 -1
- package/lib/process.js +28 -13
- package/lib/process.js.map +1 -1
- package/lib/progress.js +3 -3
- package/lib/progress.js.map +1 -1
- package/lib/strings.js +9 -9
- package/lib/strings.js.map +1 -1
- package/lib/types.d.ts +5 -1
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js +8 -1
- package/lib/types.js.map +1 -1
- package/lib/uint.js +4 -4
- package/lib/uint.js.map +1 -1
- package/lib/uri.js +8 -8
- package/lib/uri.js.map +1 -1
- package/lib/uuid.d.ts +1 -1
- package/lib/uuid.d.ts.map +1 -1
- package/lib/uuid.js +3 -2
- package/lib/uuid.js.map +1 -1
- package/package.json +8 -6
- package/src/ansi.ts +10 -0
- package/src/argv.ts +57 -0
- package/src/arrays.ts +328 -0
- package/src/async.ts +580 -0
- package/src/buffer.ts +290 -0
- package/src/cache.ts +37 -0
- package/src/cancellation.ts +136 -0
- package/src/charCode.ts +425 -0
- package/src/character-classifier.ts +82 -0
- package/src/const/encoding.ts +242 -0
- package/src/const/index.ts +1 -0
- package/src/date.ts +36 -0
- package/src/decorators.ts +166 -0
- package/src/disposable.ts +377 -0
- package/src/encoding.ts +278 -0
- package/src/errors.ts +192 -0
- package/src/event.ts +1024 -0
- package/src/file-uri.ts +41 -0
- package/src/filters.ts +887 -0
- package/src/functional.ts +43 -0
- package/src/glob.ts +752 -0
- package/src/hash.ts +336 -0
- package/src/iconLabels.ts +149 -0
- package/src/index.ts +40 -0
- package/src/iterator.ts +36 -0
- package/src/lifecycle.ts +48 -0
- package/src/linked-list.ts +144 -0
- package/src/linked-text.ts +55 -0
- package/src/lru-map.ts +133 -0
- package/src/map.ts +934 -0
- package/src/marshalling.ts +62 -0
- package/src/objects.ts +145 -0
- package/src/os.ts +76 -0
- package/src/path.ts +1885 -0
- package/src/platform.ts +194 -0
- package/src/process.ts +43 -0
- package/src/progress.ts +83 -0
- package/src/promises.ts +23 -0
- package/src/sequence.ts +28 -0
- package/src/strings.ts +1002 -0
- package/src/types.ts +214 -0
- package/src/uint.ts +59 -0
- package/src/uri.ts +298 -0
- package/src/uuid.ts +6 -0
package/src/filters.ts
ADDED
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
/* ---------------------------------------------------------------------------------------------
|
|
2
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
4
|
+
*--------------------------------------------------------------------------------------------*/
|
|
5
|
+
// Some code copied and modified from https://github.com/microsoft/vscode/blob/1.44.0/src/vs/base/common/filters.ts
|
|
6
|
+
|
|
7
|
+
import { CharCode } from './charCode';
|
|
8
|
+
import { LRUCache } from './map';
|
|
9
|
+
import { startsWithIgnoreCase, convertSimple2RegExpPattern } from './strings';
|
|
10
|
+
|
|
11
|
+
// Returns null if word doesn't match.
|
|
12
|
+
export type IFilter = (word: string, wordToMatchAgainst: string) => IMatch[] | null;
|
|
13
|
+
|
|
14
|
+
export interface IMatch {
|
|
15
|
+
start: number;
|
|
16
|
+
end: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Combined filters
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @returns A filter which combines the provided set
|
|
23
|
+
* of filters with an or. The *first* filters that
|
|
24
|
+
* matches defined the return value of the returned
|
|
25
|
+
* filter.
|
|
26
|
+
*/
|
|
27
|
+
export function or(...filter: IFilter[]): IFilter {
|
|
28
|
+
return function (word: string, wordToMatchAgainst: string): IMatch[] | null {
|
|
29
|
+
for (let i = 0, len = filter.length; i < len; i++) {
|
|
30
|
+
const match = filter[i](word, wordToMatchAgainst);
|
|
31
|
+
if (match) {
|
|
32
|
+
return match;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Prefix
|
|
40
|
+
|
|
41
|
+
export const matchesStrictPrefix: IFilter = _matchesPrefix.bind(undefined, false);
|
|
42
|
+
export const matchesPrefix: IFilter = _matchesPrefix.bind(undefined, true);
|
|
43
|
+
|
|
44
|
+
function _matchesPrefix(ignoreCase: boolean, word: string, wordToMatchAgainst: string): IMatch[] | null {
|
|
45
|
+
if (!wordToMatchAgainst || wordToMatchAgainst.length < word.length) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let matches: boolean;
|
|
50
|
+
if (ignoreCase) {
|
|
51
|
+
matches = startsWithIgnoreCase(wordToMatchAgainst, word);
|
|
52
|
+
} else {
|
|
53
|
+
matches = wordToMatchAgainst.indexOf(word) === 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!matches) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return word.length > 0 ? [{ start: 0, end: word.length }] : [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Contiguous Substring
|
|
64
|
+
|
|
65
|
+
export function matchesContiguousSubString(word: string, wordToMatchAgainst: string): IMatch[] | null {
|
|
66
|
+
const index = wordToMatchAgainst.toLowerCase().indexOf(word.toLowerCase());
|
|
67
|
+
if (index === -1) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return [{ start: index, end: index + word.length }];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Substring
|
|
75
|
+
|
|
76
|
+
export function matchesSubString(word: string, wordToMatchAgainst: string): IMatch[] | null {
|
|
77
|
+
return _matchesSubString(word.toLowerCase(), wordToMatchAgainst.toLowerCase(), 0, 0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function _matchesSubString(word: string, wordToMatchAgainst: string, i: number, j: number): IMatch[] | null {
|
|
81
|
+
if (i === word.length) {
|
|
82
|
+
return [];
|
|
83
|
+
} else if (j === wordToMatchAgainst.length) {
|
|
84
|
+
return null;
|
|
85
|
+
} else {
|
|
86
|
+
if (word[i] === wordToMatchAgainst[j]) {
|
|
87
|
+
let result: IMatch[] | null = null;
|
|
88
|
+
if ((result = _matchesSubString(word, wordToMatchAgainst, i + 1, j + 1))) {
|
|
89
|
+
return join({ start: j, end: j + 1 }, result);
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return _matchesSubString(word, wordToMatchAgainst, i, j + 1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// CamelCase
|
|
99
|
+
|
|
100
|
+
function isLower(code: number): boolean {
|
|
101
|
+
return CharCode.a <= code && code <= CharCode.z;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function isUpper(code: number): boolean {
|
|
105
|
+
return CharCode.A <= code && code <= CharCode.Z;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isNumber(code: number): boolean {
|
|
109
|
+
return CharCode.Digit0 <= code && code <= CharCode.Digit9;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isWhitespace(code: number): boolean {
|
|
113
|
+
return (
|
|
114
|
+
code === CharCode.Space || code === CharCode.Tab || code === CharCode.LineFeed || code === CharCode.CarriageReturn
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const wordSeparators = new Set<number>();
|
|
119
|
+
'`~!@#$%^&*()-=+[{]}\\|;:\'",.<>/?'.split('').forEach((s) => wordSeparators.add(s.charCodeAt(0)));
|
|
120
|
+
|
|
121
|
+
function isWordSeparator(code: number): boolean {
|
|
122
|
+
return isWhitespace(code) || wordSeparators.has(code);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function charactersMatch(codeA: number, codeB: number): boolean {
|
|
126
|
+
return codeA === codeB || (isWordSeparator(codeA) && isWordSeparator(codeB));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isAlphanumeric(code: number): boolean {
|
|
130
|
+
return isLower(code) || isUpper(code) || isNumber(code);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function join(head: IMatch, tail: IMatch[]): IMatch[] {
|
|
134
|
+
if (tail.length === 0) {
|
|
135
|
+
tail = [head];
|
|
136
|
+
} else if (head.end === tail[0].start) {
|
|
137
|
+
tail[0].start = head.start;
|
|
138
|
+
} else {
|
|
139
|
+
tail.unshift(head);
|
|
140
|
+
}
|
|
141
|
+
return tail;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function nextAnchor(camelCaseWord: string, start: number): number {
|
|
145
|
+
for (let i = start; i < camelCaseWord.length; i++) {
|
|
146
|
+
const c = camelCaseWord.charCodeAt(i);
|
|
147
|
+
if (isUpper(c) || isNumber(c) || (i > 0 && !isAlphanumeric(camelCaseWord.charCodeAt(i - 1)))) {
|
|
148
|
+
return i;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return camelCaseWord.length;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function _matchesCamelCase(word: string, camelCaseWord: string, i: number, j: number): IMatch[] | null {
|
|
155
|
+
if (i === word.length) {
|
|
156
|
+
return [];
|
|
157
|
+
} else if (j === camelCaseWord.length) {
|
|
158
|
+
return null;
|
|
159
|
+
} else if (word[i] !== camelCaseWord[j].toLowerCase()) {
|
|
160
|
+
return null;
|
|
161
|
+
} else {
|
|
162
|
+
let result: IMatch[] | null = null;
|
|
163
|
+
let nextUpperIndex = j + 1;
|
|
164
|
+
result = _matchesCamelCase(word, camelCaseWord, i + 1, j + 1);
|
|
165
|
+
while (!result && (nextUpperIndex = nextAnchor(camelCaseWord, nextUpperIndex)) < camelCaseWord.length) {
|
|
166
|
+
result = _matchesCamelCase(word, camelCaseWord, i + 1, nextUpperIndex);
|
|
167
|
+
nextUpperIndex++;
|
|
168
|
+
}
|
|
169
|
+
return result === null ? null : join({ start: j, end: j + 1 }, result);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
interface ICamelCaseAnalysis {
|
|
174
|
+
upperPercent: number;
|
|
175
|
+
lowerPercent: number;
|
|
176
|
+
alphaPercent: number;
|
|
177
|
+
numericPercent: number;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Heuristic to avoid computing camel case matcher for words that don't
|
|
181
|
+
// look like camelCaseWords.
|
|
182
|
+
function analyzeCamelCaseWord(word: string): ICamelCaseAnalysis {
|
|
183
|
+
let upper = 0;
|
|
184
|
+
let lower = 0;
|
|
185
|
+
let alpha = 0;
|
|
186
|
+
let numeric = 0;
|
|
187
|
+
let code = 0;
|
|
188
|
+
|
|
189
|
+
for (let i = 0; i < word.length; i++) {
|
|
190
|
+
code = word.charCodeAt(i);
|
|
191
|
+
|
|
192
|
+
if (isUpper(code)) {
|
|
193
|
+
upper++;
|
|
194
|
+
}
|
|
195
|
+
if (isLower(code)) {
|
|
196
|
+
lower++;
|
|
197
|
+
}
|
|
198
|
+
if (isAlphanumeric(code)) {
|
|
199
|
+
alpha++;
|
|
200
|
+
}
|
|
201
|
+
if (isNumber(code)) {
|
|
202
|
+
numeric++;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const upperPercent = upper / word.length;
|
|
207
|
+
const lowerPercent = lower / word.length;
|
|
208
|
+
const alphaPercent = alpha / word.length;
|
|
209
|
+
const numericPercent = numeric / word.length;
|
|
210
|
+
|
|
211
|
+
return { upperPercent, lowerPercent, alphaPercent, numericPercent };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isUpperCaseWord(analysis: ICamelCaseAnalysis): boolean {
|
|
215
|
+
const { upperPercent, lowerPercent } = analysis;
|
|
216
|
+
return lowerPercent === 0 && upperPercent > 0.6;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function isCamelCaseWord(analysis: ICamelCaseAnalysis): boolean {
|
|
220
|
+
const { upperPercent, lowerPercent, alphaPercent, numericPercent } = analysis;
|
|
221
|
+
return lowerPercent > 0.2 && upperPercent < 0.8 && alphaPercent > 0.6 && numericPercent < 0.2;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Heuristic to avoid computing camel case matcher for words that don't
|
|
225
|
+
// look like camel case patterns.
|
|
226
|
+
function isCamelCasePattern(word: string): boolean {
|
|
227
|
+
let upper = 0;
|
|
228
|
+
let lower = 0;
|
|
229
|
+
let code = 0;
|
|
230
|
+
let whitespace = 0;
|
|
231
|
+
|
|
232
|
+
for (let i = 0; i < word.length; i++) {
|
|
233
|
+
code = word.charCodeAt(i);
|
|
234
|
+
|
|
235
|
+
if (isUpper(code)) {
|
|
236
|
+
upper++;
|
|
237
|
+
}
|
|
238
|
+
if (isLower(code)) {
|
|
239
|
+
lower++;
|
|
240
|
+
}
|
|
241
|
+
if (isWhitespace(code)) {
|
|
242
|
+
whitespace++;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if ((upper === 0 || lower === 0) && whitespace === 0) {
|
|
247
|
+
return word.length <= 30;
|
|
248
|
+
} else {
|
|
249
|
+
return upper <= 5;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function matchesCamelCase(word: string, camelCaseWord: string): IMatch[] | null {
|
|
254
|
+
if (!camelCaseWord) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
camelCaseWord = camelCaseWord.trim();
|
|
259
|
+
|
|
260
|
+
if (camelCaseWord.length === 0) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!isCamelCasePattern(word)) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (camelCaseWord.length > 60) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const analysis = analyzeCamelCaseWord(camelCaseWord);
|
|
273
|
+
|
|
274
|
+
if (!isCamelCaseWord(analysis)) {
|
|
275
|
+
if (!isUpperCaseWord(analysis)) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
camelCaseWord = camelCaseWord.toLowerCase();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let result: IMatch[] | null = null;
|
|
283
|
+
let i = 0;
|
|
284
|
+
|
|
285
|
+
word = word.toLowerCase();
|
|
286
|
+
while (i < camelCaseWord.length && (result = _matchesCamelCase(word, camelCaseWord, 0, i)) === null) {
|
|
287
|
+
i = nextAnchor(camelCaseWord, i + 1);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Matches beginning of words supporting non-ASCII languages
|
|
294
|
+
// If `contiguous` is true then matches word with beginnings of the words in the target. E.g. "pul" will match "Git: Pull"
|
|
295
|
+
// Otherwise also matches sub string of the word with beginnings of the words in the target. E.g. "gp" or "g p" will match "Git: Pull"
|
|
296
|
+
// Useful in cases where the target is words (e.g. command labels)
|
|
297
|
+
|
|
298
|
+
export function matchesWords(word: string, target: string, contiguous = false): IMatch[] | null {
|
|
299
|
+
if (!target || target.length === 0) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let result: IMatch[] | null = null;
|
|
304
|
+
let i = 0;
|
|
305
|
+
|
|
306
|
+
word = word.toLowerCase();
|
|
307
|
+
target = target.toLowerCase();
|
|
308
|
+
while (i < target.length && (result = _matchesWords(word, target, 0, i, contiguous)) === null) {
|
|
309
|
+
i = nextWord(target, i + 1);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function _matchesWords(word: string, target: string, i: number, j: number, contiguous: boolean): IMatch[] | null {
|
|
316
|
+
if (i === word.length) {
|
|
317
|
+
return [];
|
|
318
|
+
} else if (j === target.length) {
|
|
319
|
+
return null;
|
|
320
|
+
} else if (!charactersMatch(word.charCodeAt(i), target.charCodeAt(j))) {
|
|
321
|
+
return null;
|
|
322
|
+
} else {
|
|
323
|
+
let result: IMatch[] | null = null;
|
|
324
|
+
let nextWordIndex = j + 1;
|
|
325
|
+
result = _matchesWords(word, target, i + 1, j + 1, contiguous);
|
|
326
|
+
if (!contiguous) {
|
|
327
|
+
while (!result && (nextWordIndex = nextWord(target, nextWordIndex)) < target.length) {
|
|
328
|
+
result = _matchesWords(word, target, i + 1, nextWordIndex, contiguous);
|
|
329
|
+
nextWordIndex++;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return result === null ? null : join({ start: j, end: j + 1 }, result);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function nextWord(word: string, start: number): number {
|
|
337
|
+
for (let i = start; i < word.length; i++) {
|
|
338
|
+
if (isWordSeparator(word.charCodeAt(i)) || (i > 0 && isWordSeparator(word.charCodeAt(i - 1)))) {
|
|
339
|
+
return i;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return word.length;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Fuzzy
|
|
346
|
+
|
|
347
|
+
const fuzzyContiguousFilter = or(matchesPrefix, matchesCamelCase, matchesContiguousSubString);
|
|
348
|
+
const fuzzySeparateFilter = or(matchesPrefix, matchesCamelCase, matchesSubString);
|
|
349
|
+
const fuzzyRegExpCache = new LRUCache<string, RegExp>(10000); // bounded to 10000 elements
|
|
350
|
+
|
|
351
|
+
export function matchesFuzzy(
|
|
352
|
+
word: string,
|
|
353
|
+
wordToMatchAgainst: string,
|
|
354
|
+
enableSeparateSubstringMatching = false,
|
|
355
|
+
): IMatch[] | null {
|
|
356
|
+
if (typeof word !== 'string' || typeof wordToMatchAgainst !== 'string') {
|
|
357
|
+
return null; // return early for invalid input
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Form RegExp for wildcard matches
|
|
361
|
+
let regexp = fuzzyRegExpCache.get(word);
|
|
362
|
+
if (!regexp) {
|
|
363
|
+
regexp = new RegExp(convertSimple2RegExpPattern(word), 'i');
|
|
364
|
+
fuzzyRegExpCache.set(word, regexp);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// RegExp Filter
|
|
368
|
+
const match = regexp.exec(wordToMatchAgainst);
|
|
369
|
+
if (match) {
|
|
370
|
+
return [{ start: match.index, end: match.index + match[0].length }];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Default Filter
|
|
374
|
+
return enableSeparateSubstringMatching
|
|
375
|
+
? fuzzySeparateFilter(word, wordToMatchAgainst)
|
|
376
|
+
: fuzzyContiguousFilter(word, wordToMatchAgainst);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Match pattern againt word in a fuzzy way. As in IntelliSense and faster and more
|
|
381
|
+
* powerfull than `matchesFuzzy`
|
|
382
|
+
*/
|
|
383
|
+
export function matchesFuzzy2(pattern: string, word: string): IMatch[] | null {
|
|
384
|
+
const score = fuzzyScore(pattern, pattern.toLowerCase(), 0, word, word.toLowerCase(), 0, true);
|
|
385
|
+
return score ? createMatches(score) : null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function anyScore(
|
|
389
|
+
pattern: string,
|
|
390
|
+
lowPattern: string,
|
|
391
|
+
_patternPos: number,
|
|
392
|
+
word: string,
|
|
393
|
+
lowWord: string,
|
|
394
|
+
_wordPos: number,
|
|
395
|
+
): FuzzyScore {
|
|
396
|
+
const result = fuzzyScore(pattern, lowPattern, 0, word, lowWord, 0, true);
|
|
397
|
+
if (result) {
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
let matches = 0;
|
|
401
|
+
let score = 0;
|
|
402
|
+
let idx = _wordPos;
|
|
403
|
+
for (let patternPos = 0; patternPos < lowPattern.length && patternPos < _maxLen; ++patternPos) {
|
|
404
|
+
const wordPos = lowWord.indexOf(lowPattern.charAt(patternPos), idx);
|
|
405
|
+
if (wordPos >= 0) {
|
|
406
|
+
score += 1;
|
|
407
|
+
matches += 2 ** wordPos;
|
|
408
|
+
idx = wordPos + 1;
|
|
409
|
+
} else if (matches !== 0) {
|
|
410
|
+
// once we have started matching things
|
|
411
|
+
// we need to match the remaining pattern
|
|
412
|
+
// characters
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return [score, matches, _wordPos];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// #region --- fuzzyScore ---
|
|
420
|
+
|
|
421
|
+
export function createMatches(score: undefined | FuzzyScore): IMatch[] {
|
|
422
|
+
if (typeof score === 'undefined') {
|
|
423
|
+
return [];
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const matches = score[1].toString(2);
|
|
427
|
+
const wordStart = score[2];
|
|
428
|
+
const res: IMatch[] = [];
|
|
429
|
+
|
|
430
|
+
for (let pos = wordStart; pos < _maxLen; pos++) {
|
|
431
|
+
if (matches[matches.length - (pos + 1)] === '1') {
|
|
432
|
+
const last = res[res.length - 1];
|
|
433
|
+
if (last && last.end === pos) {
|
|
434
|
+
last.end = pos + 1;
|
|
435
|
+
} else {
|
|
436
|
+
res.push({ start: pos, end: pos + 1 });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return res;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const _maxLen = 128;
|
|
444
|
+
|
|
445
|
+
function initTable() {
|
|
446
|
+
const table: number[][] = [];
|
|
447
|
+
const row: number[] = [0];
|
|
448
|
+
for (let i = 1; i <= _maxLen; i++) {
|
|
449
|
+
row.push(-i);
|
|
450
|
+
}
|
|
451
|
+
for (let i = 0; i <= _maxLen; i++) {
|
|
452
|
+
const thisRow = row.slice(0);
|
|
453
|
+
thisRow[0] = -i;
|
|
454
|
+
table.push(thisRow);
|
|
455
|
+
}
|
|
456
|
+
return table;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const _table = initTable();
|
|
460
|
+
const _scores = initTable();
|
|
461
|
+
const _arrows = initTable() as Arrow[][];
|
|
462
|
+
const _debug = false;
|
|
463
|
+
|
|
464
|
+
function printTable(table: number[][], pattern: string, patternLen: number, word: string, wordLen: number): string {
|
|
465
|
+
function pad(s: string, n: number, pad = ' ') {
|
|
466
|
+
while (s.length < n) {
|
|
467
|
+
s = pad + s;
|
|
468
|
+
}
|
|
469
|
+
return s;
|
|
470
|
+
}
|
|
471
|
+
let ret = ` | |${word
|
|
472
|
+
.split('')
|
|
473
|
+
.map((c) => pad(c, 3))
|
|
474
|
+
.join('|')}\n`;
|
|
475
|
+
|
|
476
|
+
for (let i = 0; i <= patternLen; i++) {
|
|
477
|
+
if (i === 0) {
|
|
478
|
+
ret += ' |';
|
|
479
|
+
} else {
|
|
480
|
+
ret += `${pattern[i - 1]}|`;
|
|
481
|
+
}
|
|
482
|
+
ret +=
|
|
483
|
+
table[i]
|
|
484
|
+
.slice(0, wordLen + 1)
|
|
485
|
+
.map((n) => pad(n.toString(), 3))
|
|
486
|
+
.join('|') + '\n';
|
|
487
|
+
}
|
|
488
|
+
return ret;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function isSeparatorAtPos(value: string, index: number): boolean {
|
|
492
|
+
if (index < 0 || index >= value.length) {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
const code = value.charCodeAt(index);
|
|
496
|
+
switch (code) {
|
|
497
|
+
case CharCode.Underline:
|
|
498
|
+
case CharCode.Dash:
|
|
499
|
+
case CharCode.Period:
|
|
500
|
+
case CharCode.Space:
|
|
501
|
+
case CharCode.Slash:
|
|
502
|
+
case CharCode.Backslash:
|
|
503
|
+
case CharCode.SingleQuote:
|
|
504
|
+
case CharCode.DoubleQuote:
|
|
505
|
+
case CharCode.Colon:
|
|
506
|
+
case CharCode.DollarSign:
|
|
507
|
+
return true;
|
|
508
|
+
default:
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function isWhitespaceAtPos(value: string, index: number): boolean {
|
|
514
|
+
if (index < 0 || index >= value.length) {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
const code = value.charCodeAt(index);
|
|
518
|
+
switch (code) {
|
|
519
|
+
case CharCode.Space:
|
|
520
|
+
case CharCode.Tab:
|
|
521
|
+
return true;
|
|
522
|
+
default:
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean {
|
|
528
|
+
return word[pos] !== wordLow[pos];
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function isPatternInWord(
|
|
532
|
+
patternLow: string,
|
|
533
|
+
patternPos: number,
|
|
534
|
+
patternLen: number,
|
|
535
|
+
wordLow: string,
|
|
536
|
+
wordPos: number,
|
|
537
|
+
wordLen: number,
|
|
538
|
+
): boolean {
|
|
539
|
+
while (patternPos < patternLen && wordPos < wordLen) {
|
|
540
|
+
if (patternLow[patternPos] === wordLow[wordPos]) {
|
|
541
|
+
patternPos += 1;
|
|
542
|
+
}
|
|
543
|
+
wordPos += 1;
|
|
544
|
+
}
|
|
545
|
+
return patternPos === patternLen; // pattern must be exhausted
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const enum Arrow {
|
|
549
|
+
Top = 0b1,
|
|
550
|
+
Diag = 0b10,
|
|
551
|
+
Left = 0b100,
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* A tuple of three values.
|
|
556
|
+
* 0. the score
|
|
557
|
+
* 1. the matches encoded as bitmask (2^53)
|
|
558
|
+
* 2. the offset at which matching started
|
|
559
|
+
*/
|
|
560
|
+
export type FuzzyScore = [number, number, number];
|
|
561
|
+
|
|
562
|
+
export namespace FuzzyScore {
|
|
563
|
+
/**
|
|
564
|
+
* No matches and value `-100`
|
|
565
|
+
*/
|
|
566
|
+
export const Default = Object.freeze([-100, 0, 0]) as [-100, 0, 0];
|
|
567
|
+
|
|
568
|
+
export function isDefault(score?: FuzzyScore): score is [-100, 0, 0] {
|
|
569
|
+
return !score || (score[0] === -100 && score[1] === 0 && score[2] === 0);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export type FuzzyScorer = (
|
|
574
|
+
pattern: string,
|
|
575
|
+
lowPattern: string,
|
|
576
|
+
patternPos: number,
|
|
577
|
+
word: string,
|
|
578
|
+
lowWord: string,
|
|
579
|
+
wordPos: number,
|
|
580
|
+
firstMatchCanBeWeak: boolean,
|
|
581
|
+
) => FuzzyScore | undefined;
|
|
582
|
+
|
|
583
|
+
export function fuzzyScore(
|
|
584
|
+
pattern: string,
|
|
585
|
+
patternLow: string,
|
|
586
|
+
patternPos: number,
|
|
587
|
+
word: string,
|
|
588
|
+
wordLow: string,
|
|
589
|
+
wordPos: number,
|
|
590
|
+
firstMatchCanBeWeak: boolean,
|
|
591
|
+
): FuzzyScore | undefined {
|
|
592
|
+
const patternLen = pattern.length > _maxLen ? _maxLen : pattern.length;
|
|
593
|
+
const wordLen = word.length > _maxLen ? _maxLen : word.length;
|
|
594
|
+
|
|
595
|
+
if (patternPos >= patternLen || wordPos >= wordLen || patternLen > wordLen) {
|
|
596
|
+
return undefined;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Run a simple check if the characters of pattern occur
|
|
600
|
+
// (in order) at all in word. If that isn't the case we
|
|
601
|
+
// stop because no match will be possible
|
|
602
|
+
if (!isPatternInWord(patternLow, patternPos, patternLen, wordLow, wordPos, wordLen)) {
|
|
603
|
+
return undefined;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const patternStartPos = patternPos;
|
|
607
|
+
const wordStartPos = wordPos;
|
|
608
|
+
|
|
609
|
+
// There will be a mach, fill in tables
|
|
610
|
+
for (patternPos = patternStartPos + 1; patternPos <= patternLen; patternPos++) {
|
|
611
|
+
for (wordPos = 1; wordPos <= wordLen; wordPos++) {
|
|
612
|
+
let score = -1;
|
|
613
|
+
if (patternLow[patternPos - 1] === wordLow[wordPos - 1]) {
|
|
614
|
+
if (wordPos === patternPos - patternStartPos) {
|
|
615
|
+
// common prefix: `foobar <-> foobaz`
|
|
616
|
+
// ^^^^^
|
|
617
|
+
if (pattern[patternPos - 1] === word[wordPos - 1]) {
|
|
618
|
+
score = 7;
|
|
619
|
+
} else {
|
|
620
|
+
score = 5;
|
|
621
|
+
}
|
|
622
|
+
} else if (
|
|
623
|
+
isUpperCaseAtPos(wordPos - 1, word, wordLow) &&
|
|
624
|
+
(wordPos === 1 || !isUpperCaseAtPos(wordPos - 2, word, wordLow))
|
|
625
|
+
) {
|
|
626
|
+
// hitting upper-case: `foo <-> forOthers`
|
|
627
|
+
// ^^ ^
|
|
628
|
+
if (pattern[patternPos - 1] === word[wordPos - 1]) {
|
|
629
|
+
score = 7;
|
|
630
|
+
} else {
|
|
631
|
+
score = 5;
|
|
632
|
+
}
|
|
633
|
+
} else if (isSeparatorAtPos(wordLow, wordPos - 2) || isWhitespaceAtPos(wordLow, wordPos - 2)) {
|
|
634
|
+
// post separator: `foo <-> bar_foo`
|
|
635
|
+
// ^^^
|
|
636
|
+
score = 5;
|
|
637
|
+
} else {
|
|
638
|
+
score = 1;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
_scores[patternPos][wordPos] = score;
|
|
643
|
+
|
|
644
|
+
const diag = _table[patternPos - 1][wordPos - 1] + (score > 1 ? 1 : score);
|
|
645
|
+
const top = _table[patternPos - 1][wordPos] + -1;
|
|
646
|
+
const left = _table[patternPos][wordPos - 1] + -1;
|
|
647
|
+
|
|
648
|
+
if (left >= top) {
|
|
649
|
+
// left or diag
|
|
650
|
+
if (left > diag) {
|
|
651
|
+
_table[patternPos][wordPos] = left;
|
|
652
|
+
_arrows[patternPos][wordPos] = Arrow.Left;
|
|
653
|
+
} else if (left === diag) {
|
|
654
|
+
_table[patternPos][wordPos] = left;
|
|
655
|
+
_arrows[patternPos][wordPos] = Arrow.Left | Arrow.Diag;
|
|
656
|
+
} else {
|
|
657
|
+
_table[patternPos][wordPos] = diag;
|
|
658
|
+
_arrows[patternPos][wordPos] = Arrow.Diag;
|
|
659
|
+
}
|
|
660
|
+
} else {
|
|
661
|
+
// top or diag
|
|
662
|
+
if (top > diag) {
|
|
663
|
+
_table[patternPos][wordPos] = top;
|
|
664
|
+
_arrows[patternPos][wordPos] = Arrow.Top;
|
|
665
|
+
} else if (top === diag) {
|
|
666
|
+
_table[patternPos][wordPos] = top;
|
|
667
|
+
_arrows[patternPos][wordPos] = Arrow.Top | Arrow.Diag;
|
|
668
|
+
} else {
|
|
669
|
+
_table[patternPos][wordPos] = diag;
|
|
670
|
+
_arrows[patternPos][wordPos] = Arrow.Diag;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (_debug) {
|
|
677
|
+
// eslint-disable-next-line no-console
|
|
678
|
+
console.log(printTable(_table, pattern, patternLen, word, wordLen));
|
|
679
|
+
// eslint-disable-next-line no-console
|
|
680
|
+
console.log(printTable(_arrows, pattern, patternLen, word, wordLen));
|
|
681
|
+
// eslint-disable-next-line no-console
|
|
682
|
+
console.log(printTable(_scores, pattern, patternLen, word, wordLen));
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
_matchesCount = 0;
|
|
686
|
+
_topScore = -100;
|
|
687
|
+
_patternStartPos = patternStartPos;
|
|
688
|
+
_firstMatchCanBeWeak = firstMatchCanBeWeak;
|
|
689
|
+
_findAllMatches2(patternLen, wordLen, patternLen === wordLen ? 1 : 0, 0, false);
|
|
690
|
+
if (_matchesCount === 0) {
|
|
691
|
+
return undefined;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return [_topScore, _topMatch2, wordStartPos];
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
let _matchesCount = 0;
|
|
698
|
+
let _topMatch2 = 0;
|
|
699
|
+
let _topScore = 0;
|
|
700
|
+
let _patternStartPos = 0;
|
|
701
|
+
let _firstMatchCanBeWeak = false;
|
|
702
|
+
|
|
703
|
+
function _findAllMatches2(
|
|
704
|
+
patternPos: number,
|
|
705
|
+
wordPos: number,
|
|
706
|
+
total: number,
|
|
707
|
+
matches: number,
|
|
708
|
+
lastMatched: boolean,
|
|
709
|
+
): void {
|
|
710
|
+
if (_matchesCount >= 10 || total < -25) {
|
|
711
|
+
// stop when having already 10 results, or
|
|
712
|
+
// when a potential alignment as already 5 gaps
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
let simpleMatchCount = 0;
|
|
717
|
+
|
|
718
|
+
while (patternPos > _patternStartPos && wordPos > 0) {
|
|
719
|
+
const score = _scores[patternPos][wordPos];
|
|
720
|
+
const arrow = _arrows[patternPos][wordPos];
|
|
721
|
+
|
|
722
|
+
if (arrow === Arrow.Left) {
|
|
723
|
+
// left -> no match, skip a word character
|
|
724
|
+
wordPos -= 1;
|
|
725
|
+
if (lastMatched) {
|
|
726
|
+
total -= 5; // new gap penalty
|
|
727
|
+
} else if (matches !== 0) {
|
|
728
|
+
total -= 1; // gap penalty after first match
|
|
729
|
+
}
|
|
730
|
+
lastMatched = false;
|
|
731
|
+
simpleMatchCount = 0;
|
|
732
|
+
} else if (arrow & Arrow.Diag) {
|
|
733
|
+
if (arrow & Arrow.Left) {
|
|
734
|
+
// left
|
|
735
|
+
_findAllMatches2(
|
|
736
|
+
patternPos,
|
|
737
|
+
wordPos - 1,
|
|
738
|
+
matches !== 0 ? total - 1 : total, // gap penalty after first match
|
|
739
|
+
matches,
|
|
740
|
+
lastMatched,
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// diag
|
|
745
|
+
total += score;
|
|
746
|
+
patternPos -= 1;
|
|
747
|
+
wordPos -= 1;
|
|
748
|
+
lastMatched = true;
|
|
749
|
+
|
|
750
|
+
// match -> set a 1 at the word pos
|
|
751
|
+
matches += 2 ** wordPos;
|
|
752
|
+
|
|
753
|
+
// count simple matches and boost a row of
|
|
754
|
+
// simple matches when they yield in a
|
|
755
|
+
// strong match.
|
|
756
|
+
if (score === 1) {
|
|
757
|
+
simpleMatchCount += 1;
|
|
758
|
+
|
|
759
|
+
if (patternPos === _patternStartPos && !_firstMatchCanBeWeak) {
|
|
760
|
+
// when the first match is a weak
|
|
761
|
+
// match we discard it
|
|
762
|
+
return undefined;
|
|
763
|
+
}
|
|
764
|
+
} else {
|
|
765
|
+
// boost
|
|
766
|
+
total += 1 + simpleMatchCount * (score - 1);
|
|
767
|
+
simpleMatchCount = 0;
|
|
768
|
+
}
|
|
769
|
+
} else {
|
|
770
|
+
return undefined;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
total -= wordPos >= 3 ? 9 : wordPos * 3; // late start penalty
|
|
775
|
+
|
|
776
|
+
// dynamically keep track of the current top score
|
|
777
|
+
// and insert the current best score at head, the rest at tail
|
|
778
|
+
_matchesCount += 1;
|
|
779
|
+
if (total > _topScore) {
|
|
780
|
+
_topScore = total;
|
|
781
|
+
_topMatch2 = matches;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// #endregion
|
|
786
|
+
|
|
787
|
+
// #region --- graceful ---
|
|
788
|
+
|
|
789
|
+
export function fuzzyScoreGracefulAggressive(
|
|
790
|
+
pattern: string,
|
|
791
|
+
lowPattern: string,
|
|
792
|
+
patternPos: number,
|
|
793
|
+
word: string,
|
|
794
|
+
lowWord: string,
|
|
795
|
+
wordPos: number,
|
|
796
|
+
firstMatchCanBeWeak: boolean,
|
|
797
|
+
): FuzzyScore | undefined {
|
|
798
|
+
return fuzzyScoreWithPermutations(pattern, lowPattern, patternPos, word, lowWord, wordPos, true, firstMatchCanBeWeak);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
export function fuzzyScoreGraceful(
|
|
802
|
+
pattern: string,
|
|
803
|
+
lowPattern: string,
|
|
804
|
+
patternPos: number,
|
|
805
|
+
word: string,
|
|
806
|
+
lowWord: string,
|
|
807
|
+
wordPos: number,
|
|
808
|
+
firstMatchCanBeWeak: boolean,
|
|
809
|
+
): FuzzyScore | undefined {
|
|
810
|
+
return fuzzyScoreWithPermutations(
|
|
811
|
+
pattern,
|
|
812
|
+
lowPattern,
|
|
813
|
+
patternPos,
|
|
814
|
+
word,
|
|
815
|
+
lowWord,
|
|
816
|
+
wordPos,
|
|
817
|
+
false,
|
|
818
|
+
firstMatchCanBeWeak,
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function fuzzyScoreWithPermutations(
|
|
823
|
+
pattern: string,
|
|
824
|
+
lowPattern: string,
|
|
825
|
+
patternPos: number,
|
|
826
|
+
word: string,
|
|
827
|
+
lowWord: string,
|
|
828
|
+
wordPos: number,
|
|
829
|
+
aggressive: boolean,
|
|
830
|
+
firstMatchCanBeWeak: boolean,
|
|
831
|
+
): FuzzyScore | undefined {
|
|
832
|
+
let top = fuzzyScore(pattern, lowPattern, patternPos, word, lowWord, wordPos, firstMatchCanBeWeak);
|
|
833
|
+
|
|
834
|
+
if (top && !aggressive) {
|
|
835
|
+
// when using the original pattern yield a result we`
|
|
836
|
+
// return it unless we are aggressive and try to find
|
|
837
|
+
// a better alignment, e.g. `cno` -> `^co^ns^ole` or `^c^o^nsole`.
|
|
838
|
+
return top;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (pattern.length >= 3) {
|
|
842
|
+
// When the pattern is long enough then try a few (max 7)
|
|
843
|
+
// permutations of the pattern to find a better match. The
|
|
844
|
+
// permutations only swap neighbouring characters, e.g
|
|
845
|
+
// `cnoso` becomes `conso`, `cnsoo`, `cnoos`.
|
|
846
|
+
const tries = Math.min(7, pattern.length - 1);
|
|
847
|
+
for (let movingPatternPos = patternPos + 1; movingPatternPos < tries; movingPatternPos++) {
|
|
848
|
+
const newPattern = nextTypoPermutation(pattern, movingPatternPos);
|
|
849
|
+
if (newPattern) {
|
|
850
|
+
const candidate = fuzzyScore(
|
|
851
|
+
newPattern,
|
|
852
|
+
newPattern.toLowerCase(),
|
|
853
|
+
patternPos,
|
|
854
|
+
word,
|
|
855
|
+
lowWord,
|
|
856
|
+
wordPos,
|
|
857
|
+
firstMatchCanBeWeak,
|
|
858
|
+
);
|
|
859
|
+
if (candidate) {
|
|
860
|
+
candidate[0] -= 3; // permutation penalty
|
|
861
|
+
if (!top || candidate[0] > top[0]) {
|
|
862
|
+
top = candidate;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return top;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function nextTypoPermutation(pattern: string, patternPos: number): string | undefined {
|
|
873
|
+
if (patternPos + 1 >= pattern.length) {
|
|
874
|
+
return undefined;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const swap1 = pattern[patternPos];
|
|
878
|
+
const swap2 = pattern[patternPos + 1];
|
|
879
|
+
|
|
880
|
+
if (swap1 === swap2) {
|
|
881
|
+
return undefined;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
return pattern.slice(0, patternPos) + swap2 + swap1 + pattern.slice(patternPos + 2);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// #endregion
|