@inshapardaz/likhari-react 0.1.25-dev.226 → 0.1.25-dev.230
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/dist/autocomplete/AutocompletePlugin.d.ts.map +1 -1
- package/dist/autocomplete/AutocompletePlugin.js +68 -10
- package/dist/autocomplete/completionStores.d.ts +16 -7
- package/dist/autocomplete/completionStores.d.ts.map +1 -1
- package/dist/autocomplete/completionStores.js +51 -6
- package/dist/autocomplete/wordIndex.d.ts +11 -4
- package/dist/autocomplete/wordIndex.d.ts.map +1 -1
- package/dist/autocomplete/wordIndex.js +24 -10
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AutocompletePlugin.d.ts","sourceRoot":"","sources":["../../src/autocomplete/AutocompletePlugin.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAGrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"AutocompletePlugin.d.ts","sourceRoot":"","sources":["../../src/autocomplete/AutocompletePlugin.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAGrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAsB1D;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,MAAM,EACN,QAAQ,EAAE,cAAc,GACzB,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,8EAA8E;IAC9E,QAAQ,EAAE,aAAa,GAAG,MAAM,CAAC;CAClC,sCAgNA"}
|
|
@@ -33,11 +33,35 @@ export function AutocompletePlugin({ label, stores, language: chosenLanguage, })
|
|
|
33
33
|
popupRef.current = next;
|
|
34
34
|
setPopup(next);
|
|
35
35
|
};
|
|
36
|
+
/** Counts an accepted completion in every store that keeps counts, then drops the cached lists. */
|
|
37
|
+
const recordAcceptance = (language, word) => {
|
|
38
|
+
for (const store of storesRef.current) {
|
|
39
|
+
store.recordAccept?.(language, word).catch(() => undefined);
|
|
40
|
+
}
|
|
41
|
+
indexes.current.clear();
|
|
42
|
+
};
|
|
36
43
|
useEffect(() => {
|
|
37
44
|
const indexFor = (language) => {
|
|
38
45
|
let index = indexes.current.get(language);
|
|
39
46
|
if (!index) {
|
|
40
|
-
index = Promise.all(storesRef.current.map((store) =>
|
|
47
|
+
index = Promise.all(storesRef.current.map(async (store) => {
|
|
48
|
+
try {
|
|
49
|
+
const [words, counts] = await Promise.all([
|
|
50
|
+
store.load(language),
|
|
51
|
+
store.loadCounts ? store.loadCounts(language) : Promise.resolve({}),
|
|
52
|
+
]);
|
|
53
|
+
return { words, counts };
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return { words: [], counts: {} };
|
|
57
|
+
}
|
|
58
|
+
})).then((parts) => {
|
|
59
|
+
const counts = {};
|
|
60
|
+
for (const part of parts)
|
|
61
|
+
for (const [word, count] of Object.entries(part.counts))
|
|
62
|
+
counts[word] = (counts[word] ?? 0) + count;
|
|
63
|
+
return new WordIndex(parts.flatMap((part) => part.words), counts);
|
|
64
|
+
});
|
|
41
65
|
indexes.current.set(language, index);
|
|
42
66
|
}
|
|
43
67
|
return index;
|
|
@@ -67,26 +91,37 @@ export function AutocompletePlugin({ label, stores, language: chosenLanguage, })
|
|
|
67
91
|
const { prefix, rtl } = target;
|
|
68
92
|
const language = languageRef.current === 'auto' ? (rtl ? 'ur' : 'en') : languageRef.current;
|
|
69
93
|
void indexFor(language).then((loaded) => {
|
|
94
|
+
// Same word as the open list: keep the user's highlight rather than resetting it.
|
|
95
|
+
const open = popupRef.current;
|
|
96
|
+
if (open && open.prefix === prefix && open.dir === (rtl ? 'rtl' : 'ltr'))
|
|
97
|
+
return;
|
|
70
98
|
const items = loaded.complete(prefix);
|
|
71
99
|
if (items.length === 0)
|
|
72
100
|
return hide();
|
|
73
101
|
const rect = caretRect(editor);
|
|
74
|
-
show({ items, index: 0, selected: false, x: rect.left, y: rect.bottom, dir: rtl ? 'rtl' : 'ltr' });
|
|
102
|
+
show({ prefix, language, items, index: 0, selected: false, x: rect.left, y: rect.bottom, dir: rtl ? 'rtl' : 'ltr' });
|
|
75
103
|
});
|
|
76
104
|
};
|
|
77
|
-
|
|
105
|
+
// Handles an arrow key for the list, and cancels the browser's own caret movement.
|
|
106
|
+
const move = (delta, event) => {
|
|
78
107
|
const current = popupRef.current;
|
|
79
|
-
|
|
108
|
+
// Still loading the list for the word being typed: the arrow is for the list, not the caret.
|
|
109
|
+
if (!current && prefixAtCaret(editor) === null)
|
|
80
110
|
return false;
|
|
81
|
-
|
|
82
|
-
|
|
111
|
+
event?.preventDefault();
|
|
112
|
+
if (current) {
|
|
113
|
+
const count = current.items.length;
|
|
114
|
+
show({ ...current, index: (current.index + delta + count) % count, selected: true });
|
|
115
|
+
}
|
|
83
116
|
return true;
|
|
84
117
|
};
|
|
85
118
|
const onKeyDown = (event) => {
|
|
86
119
|
// Up, Down, Tab and Enter act on the list through their editor commands, which
|
|
87
120
|
// run after this listener, so the list must stay open for them.
|
|
88
121
|
const current = popupRef.current;
|
|
89
|
-
if (current && (event.key === '
|
|
122
|
+
if (current && (event.key === 'Tab' || (event.key === 'Enter' && current.selected)))
|
|
123
|
+
return;
|
|
124
|
+
if ((event.key === 'ArrowUp' || event.key === 'ArrowDown') && (current || prefixAtCaret(editor) !== null))
|
|
90
125
|
return;
|
|
91
126
|
if (event.key === 'Escape' && popupRef.current) {
|
|
92
127
|
event.preventDefault();
|
|
@@ -129,8 +164,8 @@ export function AutocompletePlugin({ label, stores, language: chosenLanguage, })
|
|
|
129
164
|
setTimeout(refresh, 0);
|
|
130
165
|
});
|
|
131
166
|
const unregisterCommands = [
|
|
132
|
-
editor.registerCommand(KEY_ARROW_DOWN_COMMAND, () => move(1), COMMAND_PRIORITY_HIGH),
|
|
133
|
-
editor.registerCommand(KEY_ARROW_UP_COMMAND, () => move(-1), COMMAND_PRIORITY_HIGH),
|
|
167
|
+
editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (event) => move(1, event), COMMAND_PRIORITY_HIGH),
|
|
168
|
+
editor.registerCommand(KEY_ARROW_UP_COMMAND, (event) => move(-1, event), COMMAND_PRIORITY_HIGH),
|
|
134
169
|
editor.registerCommand(KEY_TAB_COMMAND, (event) => acceptHighlighted(event), COMMAND_PRIORITY_HIGH),
|
|
135
170
|
editor.registerCommand(KEY_ENTER_COMMAND, (event) => (popupRef.current?.selected ? acceptHighlighted(event) : false), COMMAND_PRIORITY_HIGH),
|
|
136
171
|
];
|
|
@@ -139,7 +174,9 @@ export function AutocompletePlugin({ label, stores, language: chosenLanguage, })
|
|
|
139
174
|
if (!current)
|
|
140
175
|
return false;
|
|
141
176
|
event?.preventDefault();
|
|
142
|
-
|
|
177
|
+
const word = current.items[current.index];
|
|
178
|
+
acceptItem(editor, word);
|
|
179
|
+
recordAcceptance(current.language, word);
|
|
143
180
|
show(null);
|
|
144
181
|
return true;
|
|
145
182
|
}
|
|
@@ -161,8 +198,29 @@ export function AutocompletePlugin({ label, stores, language: chosenLanguage, })
|
|
|
161
198
|
return (_jsx("div", { className: "likhari-autocomplete", role: "listbox", "aria-label": label, dir: popup.dir, style: { position: 'fixed', left: popup.x, top: popup.y + 4 }, children: popup.items.map((item, i) => (_jsx("div", { role: "option", "aria-selected": i === popup.index, className: i === popup.index ? 'likhari-autocomplete-item likhari-autocomplete-item--active' : 'likhari-autocomplete-item', onMouseDown: (event) => {
|
|
162
199
|
event.preventDefault();
|
|
163
200
|
acceptItem(editor, item);
|
|
201
|
+
recordAcceptance(popup.language, item);
|
|
202
|
+
show(null);
|
|
164
203
|
}, children: item }, item))) }));
|
|
165
204
|
}
|
|
205
|
+
/** The typed part of the word at the caret, when the caret is at its end and the word is long enough to complete. */
|
|
206
|
+
function prefixAtCaret(editor) {
|
|
207
|
+
let prefix = null;
|
|
208
|
+
editor.getEditorState().read(() => {
|
|
209
|
+
const selection = $getSelection();
|
|
210
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed())
|
|
211
|
+
return;
|
|
212
|
+
const node = selection.anchor.getNode();
|
|
213
|
+
if (!$isTextNode(node))
|
|
214
|
+
return;
|
|
215
|
+
const text = node.getTextContent();
|
|
216
|
+
const before = text.slice(0, selection.anchor.offset);
|
|
217
|
+
const word = WORD_END.exec(before)?.[0];
|
|
218
|
+
const after = text.charAt(selection.anchor.offset);
|
|
219
|
+
if (word && word.length >= MIN_PREFIX && !(after !== '' && LETTER.test(after)))
|
|
220
|
+
prefix = word;
|
|
221
|
+
});
|
|
222
|
+
return prefix;
|
|
223
|
+
}
|
|
166
224
|
/** Where to put the popup: under the caret, or the editor's top-left if the caret has no position yet. */
|
|
167
225
|
function caretRect(editor) {
|
|
168
226
|
const selection = window.getSelection();
|
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import { type SpellLanguage } from '../spellcheck/spellDictionaries.js';
|
|
2
2
|
/**
|
|
3
|
-
* Where autocomplete words come from
|
|
4
|
-
* are combined per language
|
|
3
|
+
* Where autocomplete words come from, and how often each was accepted. The host
|
|
4
|
+
* can pass several; words are combined per language and their counts summed.
|
|
5
|
+
* A failing store is skipped.
|
|
5
6
|
*/
|
|
6
7
|
export interface CompletionStore {
|
|
7
8
|
readonly id: string;
|
|
8
9
|
load(language: SpellLanguage): Promise<string[]>;
|
|
10
|
+
/** How many times each word was accepted as a completion. */
|
|
11
|
+
loadCounts?(language: SpellLanguage): Promise<Record<string, number>>;
|
|
12
|
+
/** Records one accepted completion, so the word is suggested sooner next time. */
|
|
13
|
+
recordAccept?(language: SpellLanguage, word: string): Promise<void>;
|
|
9
14
|
}
|
|
10
15
|
/**
|
|
11
|
-
* The default: the dictionary's words plus the words the user has added.
|
|
12
|
-
*
|
|
16
|
+
* The default: the dictionary's words plus the words the user has added. Accepted
|
|
17
|
+
* completions are counted in this browser's localStorage, and the most accepted
|
|
18
|
+
* words are suggested first.
|
|
13
19
|
*/
|
|
14
|
-
export declare function dictionaryCompletionStore(
|
|
20
|
+
export declare function dictionaryCompletionStore(options?: {
|
|
21
|
+
countsKey?: string;
|
|
22
|
+
}): CompletionStore;
|
|
15
23
|
/**
|
|
16
|
-
* Words served by an API. `load` calls `GET {url}?language=xx`
|
|
17
|
-
* array of strings
|
|
24
|
+
* Words and counts served by an API. `load` calls `GET {url}?language=xx` for a JSON
|
|
25
|
+
* array of strings; `loadCounts` calls `GET {url}/counts?language=xx` for a JSON
|
|
26
|
+
* object of counts; `recordAccept` calls `POST {url}/accept` with `{ language, word }`.
|
|
18
27
|
*/
|
|
19
28
|
export declare function apiCompletionStore(options: {
|
|
20
29
|
id?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"completionStores.d.ts","sourceRoot":"","sources":["../../src/autocomplete/completionStores.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAGzF
|
|
1
|
+
{"version":3,"file":"completionStores.d.ts","sourceRoot":"","sources":["../../src/autocomplete/completionStores.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAGzF;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACjD,6DAA6D;IAC7D,UAAU,CAAC,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtE,kFAAkF;IAClF,YAAY,CAAC,CAAC,QAAQ,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrE;AAiBD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,eAAe,CAwB/F;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE;IAC1C,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB,GAAG,eAAe,CA0BlB"}
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { getDictionaryWords } from '../spellcheck/spellDictionaries.js';
|
|
2
2
|
import { acceptedWords } from '../spellcheck/userWords.js';
|
|
3
|
+
const COUNTS_KEY = 'likhari-completion-counts';
|
|
4
|
+
function readCounts(key) {
|
|
5
|
+
try {
|
|
6
|
+
const parsed = JSON.parse(localStorage.getItem(key) ?? '{}');
|
|
7
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
}
|
|
3
13
|
/**
|
|
4
|
-
* The default: the dictionary's words plus the words the user has added.
|
|
5
|
-
*
|
|
14
|
+
* The default: the dictionary's words plus the words the user has added. Accepted
|
|
15
|
+
* completions are counted in this browser's localStorage, and the most accepted
|
|
16
|
+
* words are suggested first.
|
|
6
17
|
*/
|
|
7
|
-
export function dictionaryCompletionStore() {
|
|
18
|
+
export function dictionaryCompletionStore(options = {}) {
|
|
19
|
+
const key = options.countsKey ?? COUNTS_KEY;
|
|
8
20
|
return {
|
|
9
21
|
id: 'dictionary',
|
|
10
22
|
async load(language) {
|
|
@@ -12,22 +24,55 @@ export function dictionaryCompletionStore() {
|
|
|
12
24
|
const dictionary = words ? await words : [];
|
|
13
25
|
return [...dictionary, ...acceptedWords(language)];
|
|
14
26
|
},
|
|
27
|
+
async loadCounts(language) {
|
|
28
|
+
return readCounts(key)[language] ?? {};
|
|
29
|
+
},
|
|
30
|
+
async recordAccept(language, word) {
|
|
31
|
+
const all = readCounts(key);
|
|
32
|
+
const counts = all[language] ?? {};
|
|
33
|
+
counts[word] = (counts[word] ?? 0) + 1;
|
|
34
|
+
all[language] = counts;
|
|
35
|
+
try {
|
|
36
|
+
localStorage.setItem(key, JSON.stringify(all));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Storage full or disabled: the count is kept for this session only.
|
|
40
|
+
}
|
|
41
|
+
},
|
|
15
42
|
};
|
|
16
43
|
}
|
|
17
44
|
/**
|
|
18
|
-
* Words served by an API. `load` calls `GET {url}?language=xx`
|
|
19
|
-
* array of strings
|
|
45
|
+
* Words and counts served by an API. `load` calls `GET {url}?language=xx` for a JSON
|
|
46
|
+
* array of strings; `loadCounts` calls `GET {url}/counts?language=xx` for a JSON
|
|
47
|
+
* object of counts; `recordAccept` calls `POST {url}/accept` with `{ language, word }`.
|
|
20
48
|
*/
|
|
21
49
|
export function apiCompletionStore(options) {
|
|
22
50
|
const doFetch = options.fetch ?? fetch;
|
|
51
|
+
const query = (language) => `?language=${encodeURIComponent(language)}`;
|
|
23
52
|
return {
|
|
24
53
|
id: options.id ?? 'api',
|
|
25
54
|
async load(language) {
|
|
26
|
-
const response = await doFetch(`${options.url}
|
|
55
|
+
const response = await doFetch(`${options.url}${query(language)}`, { headers: options.headers });
|
|
27
56
|
if (!response.ok)
|
|
28
57
|
throw new Error(`Completion API failed: ${response.status}`);
|
|
29
58
|
const data = await response.json();
|
|
30
59
|
return Array.isArray(data) ? data.filter((w) => typeof w === 'string') : [];
|
|
31
60
|
},
|
|
61
|
+
async loadCounts(language) {
|
|
62
|
+
const response = await doFetch(`${options.url}/counts${query(language)}`, { headers: options.headers });
|
|
63
|
+
if (!response.ok)
|
|
64
|
+
throw new Error(`Completion counts failed: ${response.status}`);
|
|
65
|
+
const data = await response.json();
|
|
66
|
+
return data && typeof data === 'object' ? data : {};
|
|
67
|
+
},
|
|
68
|
+
async recordAccept(language, word) {
|
|
69
|
+
const response = await doFetch(`${options.url}/accept`, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: { 'Content-Type': 'application/json', ...options.headers },
|
|
72
|
+
body: JSON.stringify({ language, word }),
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok)
|
|
75
|
+
throw new Error(`Completion API rejected the count: ${response.status}`);
|
|
76
|
+
},
|
|
32
77
|
};
|
|
33
78
|
}
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Completion words, sorted once so every prefix lookup is a binary search. Each
|
|
3
|
+
* word has a count of how often it was accepted as a completion; more used words
|
|
4
|
+
* are suggested first.
|
|
5
|
+
*/
|
|
2
6
|
export declare class WordIndex {
|
|
3
7
|
private readonly words;
|
|
4
|
-
|
|
8
|
+
private readonly counts;
|
|
9
|
+
constructor(words: Iterable<string>, counts?: Record<string, number>);
|
|
5
10
|
/**
|
|
6
11
|
* Completions for a prefix: words that start with it (other than the prefix
|
|
7
|
-
* itself),
|
|
8
|
-
* matches are
|
|
12
|
+
* itself), most accepted first, then shortest, then alphabetical. Only the first
|
|
13
|
+
* `scanLimit` matches of the sorted words are scanned, so a one-letter prefix stays
|
|
14
|
+
* quick; words with a count are always considered.
|
|
9
15
|
*/
|
|
10
16
|
complete(prefix: string, limit?: number, scanLimit?: number): string[];
|
|
17
|
+
private countOf;
|
|
11
18
|
private lowerBound;
|
|
12
19
|
}
|
|
13
20
|
//# sourceMappingURL=wordIndex.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wordIndex.d.ts","sourceRoot":"","sources":["../../src/autocomplete/wordIndex.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"wordIndex.d.ts","sourceRoot":"","sources":["../../src/autocomplete/wordIndex.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAW;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;gBAEjC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM;IAKxE;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,SAAI,EAAE,SAAS,SAAM,GAAG,MAAM,EAAE;IAgB9D,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,UAAU;CAUnB"}
|
|
@@ -1,26 +1,40 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Completion words, sorted once so every prefix lookup is a binary search. Each
|
|
3
|
+
* word has a count of how often it was accepted as a completion; more used words
|
|
4
|
+
* are suggested first.
|
|
5
|
+
*/
|
|
2
6
|
export class WordIndex {
|
|
3
|
-
constructor(words) {
|
|
4
|
-
this.
|
|
7
|
+
constructor(words, counts = {}) {
|
|
8
|
+
this.counts = new Map(Object.entries(counts));
|
|
9
|
+
this.words = [...new Set([...words, ...this.counts.keys()])].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
5
10
|
}
|
|
6
11
|
/**
|
|
7
12
|
* Completions for a prefix: words that start with it (other than the prefix
|
|
8
|
-
* itself),
|
|
9
|
-
* matches are
|
|
13
|
+
* itself), most accepted first, then shortest, then alphabetical. Only the first
|
|
14
|
+
* `scanLimit` matches of the sorted words are scanned, so a one-letter prefix stays
|
|
15
|
+
* quick; words with a count are always considered.
|
|
10
16
|
*/
|
|
11
17
|
complete(prefix, limit = 5, scanLimit = 500) {
|
|
12
18
|
if (prefix === '')
|
|
13
19
|
return [];
|
|
14
|
-
const
|
|
15
|
-
for (let i = this.lowerBound(prefix); i < this.words.length &&
|
|
20
|
+
const candidates = new Set();
|
|
21
|
+
for (let i = this.lowerBound(prefix), scanned = 0; i < this.words.length && scanned < scanLimit; i++) {
|
|
16
22
|
const word = this.words[i];
|
|
17
23
|
if (!word.startsWith(prefix))
|
|
18
24
|
break;
|
|
19
25
|
if (word !== prefix)
|
|
20
|
-
|
|
26
|
+
candidates.add(word);
|
|
27
|
+
scanned += 1;
|
|
21
28
|
}
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
for (const word of this.counts.keys())
|
|
30
|
+
if (word !== prefix && word.startsWith(prefix))
|
|
31
|
+
candidates.add(word);
|
|
32
|
+
return [...candidates]
|
|
33
|
+
.sort((a, b) => (this.countOf(b) - this.countOf(a)) || a.length - b.length || (a < b ? -1 : a > b ? 1 : 0))
|
|
34
|
+
.slice(0, limit);
|
|
35
|
+
}
|
|
36
|
+
countOf(word) {
|
|
37
|
+
return this.counts.get(word) ?? 0;
|
|
24
38
|
}
|
|
25
39
|
lowerBound(prefix) {
|
|
26
40
|
let low = 0;
|