@atlaskit/editor-plugin-autocomplete 4.0.4 → 4.0.6
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/CHANGELOG.md +16 -0
- package/compass.yml +41 -0
- package/dist/cjs/pm-plugins/autocomplete-plugin.js +10 -7
- package/dist/cjs/pm-plugins/local-slow-lane-client.js +3 -3
- package/dist/cjs/pm-plugins/slow-lane-client.js +3 -2
- package/dist/cjs/pm-plugins/text-predictor.js +11 -9
- package/dist/es2019/pm-plugins/autocomplete-plugin.js +10 -7
- package/dist/es2019/pm-plugins/local-slow-lane-client.js +3 -3
- package/dist/es2019/pm-plugins/slow-lane-client.js +3 -3
- package/dist/es2019/pm-plugins/text-predictor.js +11 -9
- package/dist/esm/pm-plugins/autocomplete-plugin.js +10 -7
- package/dist/esm/pm-plugins/local-slow-lane-client.js +3 -3
- package/dist/esm/pm-plugins/slow-lane-client.js +3 -2
- package/dist/esm/pm-plugins/text-predictor.js +11 -9
- package/dist/types/pm-plugins/autocomplete-plugin.d.ts +5 -5
- package/package.json +2 -2
- package/src/pm-plugins/autocomplete-plugin.ts +15 -11
- package/src/pm-plugins/local-slow-lane-client.ts +3 -2
- package/src/pm-plugins/slow-lane-client.ts +3 -2
- package/src/pm-plugins/text-predictor.ts +11 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @atlaskit/editor-plugin-autocomplete
|
|
2
2
|
|
|
3
|
+
## 4.0.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`c2986ab2c7a01`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/c2986ab2c7a01) -
|
|
8
|
+
Cleans up prefer static regex violations and enables e18e rule
|
|
9
|
+
- Updated dependencies
|
|
10
|
+
|
|
11
|
+
## 4.0.5
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [`73743eb8b36e6`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/73743eb8b36e6) -
|
|
16
|
+
CLeanup prefer static regex violations
|
|
17
|
+
- Updated dependencies
|
|
18
|
+
|
|
3
19
|
## 4.0.4
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/compass.yml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
configVersion: 1
|
|
2
|
+
id: ari:cloud:compass:a436116f-02ce-4520-8fbb-7301462a1674:component/c5751cc6-3513-4070-9deb-af31e86aed34/45ef6a6c-98b8-4615-940d-e9013db36999
|
|
3
|
+
name: '@atlaskit/editor-plugin-autocomplete'
|
|
4
|
+
ownerId: ari:cloud:identity::team/0250ee69-f755-47fd-a468-bf8a9d10a3c5 # Editor AI
|
|
5
|
+
labels:
|
|
6
|
+
- platform-code
|
|
7
|
+
- platform-afm
|
|
8
|
+
typeId: OTHER
|
|
9
|
+
fields:
|
|
10
|
+
tier: 4
|
|
11
|
+
lifecycle: Active
|
|
12
|
+
isMonorepoProject: true
|
|
13
|
+
links:
|
|
14
|
+
- name: Root Repository
|
|
15
|
+
type: REPOSITORY
|
|
16
|
+
url: https://bitbucket.org/atlassian/atlassian-frontend-monorepo/src/master
|
|
17
|
+
- name: Slack Channel
|
|
18
|
+
type: CHAT_CHANNEL
|
|
19
|
+
url: https://atlassian.enterprise.slack.com/archives/CFG3PSQ9E # #help-editor
|
|
20
|
+
- name: Editor Plugin Autocomplete
|
|
21
|
+
type: REPOSITORY
|
|
22
|
+
url: https://bitbucket.org/atlassian/atlassian-frontend-monorepo/src/master/platform/packages/editor/editor-plugin-autocomplete
|
|
23
|
+
customFields:
|
|
24
|
+
- name: Department
|
|
25
|
+
type: text
|
|
26
|
+
value: Eng - Editor AI
|
|
27
|
+
- name: Trusted Reviewer Teams
|
|
28
|
+
type: text
|
|
29
|
+
value: ari:cloud:identity::team/cf6a670b-5252-4c68-a769-9207de366beb
|
|
30
|
+
- name: Technical Owner
|
|
31
|
+
type: user
|
|
32
|
+
value: ari:cloud:identity::user/612646c53fe26c00694fbe6a # Chris Kimber
|
|
33
|
+
- name: Required Reviewers Opt In
|
|
34
|
+
type: boolean
|
|
35
|
+
value: true
|
|
36
|
+
- name: Reviewer Selection Mechanism
|
|
37
|
+
type: text
|
|
38
|
+
value: random(2)
|
|
39
|
+
- name: Required Reviewer Approvals
|
|
40
|
+
type: number
|
|
41
|
+
value: 1
|
|
@@ -38,6 +38,11 @@ var MAX_INGESTED_CONTEXT_TEXTS = 50;
|
|
|
38
38
|
// non-comment editors (where parentCommentContent never arrives) don't refetch
|
|
39
39
|
// on every word boundary for the plugin's lifetime.
|
|
40
40
|
var MAX_CONTEXT_REFRESH_ATTEMPTS = 5;
|
|
41
|
+
|
|
42
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
43
|
+
var TRAILING_NON_BOUNDARY_TOKEN_REGEX = /([^\s.,;:!?]*)$/;
|
|
44
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
45
|
+
var WORD_BOUNDARY_CHARS_REGEX = /[\s.,;:!?]/;
|
|
41
46
|
var hasDestroy = function hasDestroy(client) {
|
|
42
47
|
return 'destroy' in client && typeof client.destroy === 'function';
|
|
43
48
|
};
|
|
@@ -191,6 +196,8 @@ var buildSlowLaneText = function buildSlowLaneText(docText, context) {
|
|
|
191
196
|
var createMidWordCharacterRegex = function createMidWordCharacterRegex() {
|
|
192
197
|
try {
|
|
193
198
|
// string-built regex avoids TS1501 under the package's ES5 build target
|
|
199
|
+
// Ignored via go/ees019
|
|
200
|
+
// eslint-disable-next-line e18e/prefer-static-regex
|
|
194
201
|
return new RegExp('[\\p{L}\\p{N}_]', 'u');
|
|
195
202
|
} catch (_unused) {
|
|
196
203
|
return null;
|
|
@@ -225,9 +232,7 @@ var getTypedLengthForPrediction = function getTypedLengthForPrediction(textBefor
|
|
|
225
232
|
if ((0, _slowLaneClient.isWordBoundary)(textBefore)) {
|
|
226
233
|
return 0;
|
|
227
234
|
}
|
|
228
|
-
|
|
229
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
230
|
-
var trailingToken = (_textBefore$match$ = (_textBefore$match = textBefore.match(/([^\s.,;:!?]*)$/)) === null || _textBefore$match === void 0 ? void 0 : _textBefore$match[1]) !== null && _textBefore$match$ !== void 0 ? _textBefore$match$ : '';
|
|
235
|
+
var trailingToken = (_textBefore$match$ = (_textBefore$match = textBefore.match(TRAILING_NON_BOUNDARY_TOKEN_REGEX)) === null || _textBefore$match === void 0 ? void 0 : _textBefore$match[1]) !== null && _textBefore$match$ !== void 0 ? _textBefore$match$ : '';
|
|
231
236
|
return trailingToken.length;
|
|
232
237
|
};
|
|
233
238
|
var isEnglishLocale = function isEnglishLocale(locale) {
|
|
@@ -539,16 +544,14 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
|
|
|
539
544
|
return;
|
|
540
545
|
}
|
|
541
546
|
var lastChar = newText[newText.length - 1];
|
|
542
|
-
|
|
543
|
-
if (!/[\s.,;:!?]/.test(lastChar)) {
|
|
547
|
+
if (!WORD_BOUNDARY_CHARS_REGEX.test(lastChar)) {
|
|
544
548
|
return;
|
|
545
549
|
}
|
|
546
550
|
|
|
547
551
|
// Only fire if the previous state did not already end on a boundary,
|
|
548
552
|
// so we don't double-count when multiple boundary chars are inserted.
|
|
549
553
|
var prevLastChar = prevText[prevText.length - 1];
|
|
550
|
-
|
|
551
|
-
if (prevLastChar && /[\s.,;:!?]/.test(prevLastChar)) {
|
|
554
|
+
if (prevLastChar && WORD_BOUNDARY_CHARS_REGEX.test(prevLastChar)) {
|
|
552
555
|
return;
|
|
553
556
|
}
|
|
554
557
|
var beforeBoundary = newText.slice(0, -1).trimEnd();
|
|
@@ -78,6 +78,8 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
|
|
|
78
78
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
79
79
|
|
|
80
80
|
var DEFAULT_DEBOUNCE_MS = 300;
|
|
81
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
82
|
+
var OOM_REGEX = /\boom\b/;
|
|
81
83
|
var LOCAL_MLC_CAUSAL_MODEL_ID = exports.LOCAL_MLC_CAUSAL_MODEL_ID = 'SmolLM2-135M-Instruct-q0f16-MLC';
|
|
82
84
|
|
|
83
85
|
/**
|
|
@@ -766,9 +768,7 @@ var createLocalSlowLaneClient = exports.createLocalSlowLaneClient = function cre
|
|
|
766
768
|
if (lower.includes('loading chunk') || lower.includes('dynamically imported module') || lower.includes('dynamic import')) {
|
|
767
769
|
return 'module_load_failed';
|
|
768
770
|
}
|
|
769
|
-
if (lower.includes('out of memory') ||
|
|
770
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
771
|
-
/\boom\b/.test(lower) || lower.includes('allocation') || lower.includes('exceeds') || lower.includes('buffer size') || lower.includes('not enough memory')) {
|
|
771
|
+
if (lower.includes('out of memory') || OOM_REGEX.test(lower) || lower.includes('allocation') || lower.includes('exceeds') || lower.includes('buffer size') || lower.includes('not enough memory')) {
|
|
772
772
|
return 'insufficient_memory';
|
|
773
773
|
}
|
|
774
774
|
// Pre-flight already returns missing_shader_f16 when the feature is absent,
|
|
@@ -29,6 +29,8 @@ var _debugMode = require("./debug-mode");
|
|
|
29
29
|
|
|
30
30
|
// eslint-disable-next-line require-unicode-regexp
|
|
31
31
|
var WORD_BOUNDARY_CHARS = /[\s.,;:!?]/;
|
|
32
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
33
|
+
var TRAILING_SLASH_REGEX = /\/$/;
|
|
32
34
|
var DEFAULT_DEBOUNCE_MS = 300;
|
|
33
35
|
|
|
34
36
|
/**
|
|
@@ -80,8 +82,7 @@ var createSlowLaneClient = exports.createSlowLaneClient = function createSlowLan
|
|
|
80
82
|
}
|
|
81
83
|
return _context.abrupt("return");
|
|
82
84
|
case 1:
|
|
83
|
-
|
|
84
|
-
url = "".concat(baseUrl.replace(/\/$/, '')).concat(endpoint);
|
|
85
|
+
url = "".concat(baseUrl.replace(TRAILING_SLASH_REGEX, '')).concat(endpoint);
|
|
85
86
|
payload = {
|
|
86
87
|
text: text,
|
|
87
88
|
session_id: sessionId
|
|
@@ -42,6 +42,12 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
|
|
|
42
42
|
|
|
43
43
|
// eslint-disable-next-line require-unicode-regexp
|
|
44
44
|
var PUNCTUATION_BOUNDARY_REGEX = /^[.,;:!?()\[\]{}"'`]+|[.,;:!?()\[\]{}"'`]+$/g;
|
|
45
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
46
|
+
var WHITESPACE_SPLIT_REGEX = /\s+/;
|
|
47
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
48
|
+
var SENTENCE_BOUNDARY_REGEX = /[\n.?!]+/;
|
|
49
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
50
|
+
var TRAILING_WHITESPACE_REGEX = /\s$/;
|
|
45
51
|
var MIN_PREFIX_LENGTH = 3;
|
|
46
52
|
var MAX_CANDIDATES = 200;
|
|
47
53
|
var CONTEXT_WORDS = 10;
|
|
@@ -312,8 +318,8 @@ var getContextVectorForScoring = function getContextVectorForScoring(textBefore)
|
|
|
312
318
|
};
|
|
313
319
|
var tokenize = function tokenize(text) {
|
|
314
320
|
var tokens = [];
|
|
315
|
-
// eslint-disable-next-line
|
|
316
|
-
var _iterator7 = _createForOfIteratorHelper(text.toLowerCase().split(
|
|
321
|
+
// eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
|
|
322
|
+
var _iterator7 = _createForOfIteratorHelper(text.toLowerCase().split(WHITESPACE_SPLIT_REGEX)),
|
|
317
323
|
_step7;
|
|
318
324
|
try {
|
|
319
325
|
for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
|
|
@@ -333,12 +339,9 @@ var tokenize = function tokenize(text) {
|
|
|
333
339
|
};
|
|
334
340
|
var extractPreviousWord = function extractPreviousWord(text) {
|
|
335
341
|
// Only consider the current sentence/line the user is typing in.
|
|
336
|
-
|
|
337
|
-
var sentences = text.split(/[\n.?!]+/);
|
|
342
|
+
var sentences = text.split(SENTENCE_BOUNDARY_REGEX);
|
|
338
343
|
var currentSentence = sentences[sentences.length - 1];
|
|
339
|
-
|
|
340
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
341
|
-
var words = currentSentence.trimEnd().split(/\s+/);
|
|
344
|
+
var words = currentSentence.trimEnd().split(WHITESPACE_SPLIT_REGEX);
|
|
342
345
|
return words.length >= 2 ? words[words.length - 2] : '';
|
|
343
346
|
};
|
|
344
347
|
|
|
@@ -477,8 +480,7 @@ var predict = exports.predict = function predict(textBefore) {
|
|
|
477
480
|
// }
|
|
478
481
|
|
|
479
482
|
// ── Step 2: Prefix completion (≥3 chars typed) ──────────────────────────
|
|
480
|
-
|
|
481
|
-
if (textBefore.length > 0 && /\s$/.test(textBefore)) {
|
|
483
|
+
if (textBefore.length > 0 && TRAILING_WHITESPACE_REGEX.test(textBefore)) {
|
|
482
484
|
return null;
|
|
483
485
|
}
|
|
484
486
|
var trimmed = textBefore.trimEnd();
|
|
@@ -24,6 +24,11 @@ const MAX_INGESTED_CONTEXT_TEXTS = 50;
|
|
|
24
24
|
// non-comment editors (where parentCommentContent never arrives) don't refetch
|
|
25
25
|
// on every word boundary for the plugin's lifetime.
|
|
26
26
|
const MAX_CONTEXT_REFRESH_ATTEMPTS = 5;
|
|
27
|
+
|
|
28
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
29
|
+
const TRAILING_NON_BOUNDARY_TOKEN_REGEX = /([^\s.,;:!?]*)$/;
|
|
30
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
31
|
+
const WORD_BOUNDARY_CHARS_REGEX = /[\s.,;:!?]/;
|
|
27
32
|
const hasDestroy = client => 'destroy' in client && typeof client.destroy === 'function';
|
|
28
33
|
const createInitialState = () => ({
|
|
29
34
|
ghostText: '',
|
|
@@ -179,6 +184,8 @@ const buildSlowLaneText = (docText, context) => {
|
|
|
179
184
|
const createMidWordCharacterRegex = () => {
|
|
180
185
|
try {
|
|
181
186
|
// string-built regex avoids TS1501 under the package's ES5 build target
|
|
187
|
+
// Ignored via go/ees019
|
|
188
|
+
// eslint-disable-next-line e18e/prefer-static-regex
|
|
182
189
|
return new RegExp('[\\p{L}\\p{N}_]', 'u');
|
|
183
190
|
} catch {
|
|
184
191
|
return null;
|
|
@@ -213,9 +220,7 @@ const getTypedLengthForPrediction = textBefore => {
|
|
|
213
220
|
if (isWordBoundary(textBefore)) {
|
|
214
221
|
return 0;
|
|
215
222
|
}
|
|
216
|
-
|
|
217
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
218
|
-
const trailingToken = (_textBefore$match$ = (_textBefore$match = textBefore.match(/([^\s.,;:!?]*)$/)) === null || _textBefore$match === void 0 ? void 0 : _textBefore$match[1]) !== null && _textBefore$match$ !== void 0 ? _textBefore$match$ : '';
|
|
223
|
+
const trailingToken = (_textBefore$match$ = (_textBefore$match = textBefore.match(TRAILING_NON_BOUNDARY_TOKEN_REGEX)) === null || _textBefore$match === void 0 ? void 0 : _textBefore$match[1]) !== null && _textBefore$match$ !== void 0 ? _textBefore$match$ : '';
|
|
219
224
|
return trailingToken.length;
|
|
220
225
|
};
|
|
221
226
|
const isEnglishLocale = locale => {
|
|
@@ -523,16 +528,14 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
523
528
|
return;
|
|
524
529
|
}
|
|
525
530
|
const lastChar = newText[newText.length - 1];
|
|
526
|
-
|
|
527
|
-
if (!/[\s.,;:!?]/.test(lastChar)) {
|
|
531
|
+
if (!WORD_BOUNDARY_CHARS_REGEX.test(lastChar)) {
|
|
528
532
|
return;
|
|
529
533
|
}
|
|
530
534
|
|
|
531
535
|
// Only fire if the previous state did not already end on a boundary,
|
|
532
536
|
// so we don't double-count when multiple boundary chars are inserted.
|
|
533
537
|
const prevLastChar = prevText[prevText.length - 1];
|
|
534
|
-
|
|
535
|
-
if (prevLastChar && /[\s.,;:!?]/.test(prevLastChar)) {
|
|
538
|
+
if (prevLastChar && WORD_BOUNDARY_CHARS_REGEX.test(prevLastChar)) {
|
|
536
539
|
return;
|
|
537
540
|
}
|
|
538
541
|
const beforeBoundary = newText.slice(0, -1).trimEnd();
|
|
@@ -60,6 +60,8 @@ import { isWordBoundary } from './slow-lane-client';
|
|
|
60
60
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
61
61
|
|
|
62
62
|
const DEFAULT_DEBOUNCE_MS = 300;
|
|
63
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
64
|
+
const OOM_REGEX = /\boom\b/;
|
|
63
65
|
export const LOCAL_MLC_CAUSAL_MODEL_ID = 'SmolLM2-135M-Instruct-q0f16-MLC';
|
|
64
66
|
|
|
65
67
|
/**
|
|
@@ -587,9 +589,7 @@ export const createLocalSlowLaneClient = (config = {}) => {
|
|
|
587
589
|
if (lower.includes('loading chunk') || lower.includes('dynamically imported module') || lower.includes('dynamic import')) {
|
|
588
590
|
return 'module_load_failed';
|
|
589
591
|
}
|
|
590
|
-
if (lower.includes('out of memory') ||
|
|
591
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
592
|
-
/\boom\b/.test(lower) || lower.includes('allocation') || lower.includes('exceeds') || lower.includes('buffer size') || lower.includes('not enough memory')) {
|
|
592
|
+
if (lower.includes('out of memory') || OOM_REGEX.test(lower) || lower.includes('allocation') || lower.includes('exceeds') || lower.includes('buffer size') || lower.includes('not enough memory')) {
|
|
593
593
|
return 'insufficient_memory';
|
|
594
594
|
}
|
|
595
595
|
// Pre-flight already returns missing_shader_f16 when the feature is absent,
|
|
@@ -20,6 +20,8 @@ import { isAutocompleteDebugEnabled } from './debug-mode';
|
|
|
20
20
|
|
|
21
21
|
// eslint-disable-next-line require-unicode-regexp
|
|
22
22
|
const WORD_BOUNDARY_CHARS = /[\s.,;:!?]/;
|
|
23
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
24
|
+
const TRAILING_SLASH_REGEX = /\/$/;
|
|
23
25
|
const DEFAULT_DEBOUNCE_MS = 300;
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -61,9 +63,7 @@ export const createSlowLaneClient = config => {
|
|
|
61
63
|
if (!text || text.trim().length === 0) {
|
|
62
64
|
return;
|
|
63
65
|
}
|
|
64
|
-
|
|
65
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
66
|
-
const url = `${baseUrl.replace(/\/$/, '')}${endpoint}`;
|
|
66
|
+
const url = `${baseUrl.replace(TRAILING_SLASH_REGEX, '')}${endpoint}`;
|
|
67
67
|
const payload = {
|
|
68
68
|
text,
|
|
69
69
|
session_id: sessionId
|
|
@@ -29,6 +29,12 @@ import { getStoredContextVector, getStoredLmLogits } from './slow-lane-client';
|
|
|
29
29
|
|
|
30
30
|
// eslint-disable-next-line require-unicode-regexp
|
|
31
31
|
const PUNCTUATION_BOUNDARY_REGEX = /^[.,;:!?()\[\]{}"'`]+|[.,;:!?()\[\]{}"'`]+$/g;
|
|
32
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
33
|
+
const WHITESPACE_SPLIT_REGEX = /\s+/;
|
|
34
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
35
|
+
const SENTENCE_BOUNDARY_REGEX = /[\n.?!]+/;
|
|
36
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
37
|
+
const TRAILING_WHITESPACE_REGEX = /\s$/;
|
|
32
38
|
const MIN_PREFIX_LENGTH = 3;
|
|
33
39
|
const MAX_CANDIDATES = 200;
|
|
34
40
|
const CONTEXT_WORDS = 10;
|
|
@@ -235,8 +241,8 @@ const getContextVectorForScoring = textBefore => {
|
|
|
235
241
|
};
|
|
236
242
|
const tokenize = text => {
|
|
237
243
|
const tokens = [];
|
|
238
|
-
// eslint-disable-next-line
|
|
239
|
-
for (const raw of text.toLowerCase().split(
|
|
244
|
+
// eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
|
|
245
|
+
for (const raw of text.toLowerCase().split(WHITESPACE_SPLIT_REGEX)) {
|
|
240
246
|
// eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
|
|
241
247
|
const clean = raw.replace(PUNCTUATION_BOUNDARY_REGEX, '');
|
|
242
248
|
if (clean.length >= 2) {
|
|
@@ -247,12 +253,9 @@ const tokenize = text => {
|
|
|
247
253
|
};
|
|
248
254
|
const extractPreviousWord = text => {
|
|
249
255
|
// Only consider the current sentence/line the user is typing in.
|
|
250
|
-
|
|
251
|
-
const sentences = text.split(/[\n.?!]+/);
|
|
256
|
+
const sentences = text.split(SENTENCE_BOUNDARY_REGEX);
|
|
252
257
|
const currentSentence = sentences[sentences.length - 1];
|
|
253
|
-
|
|
254
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
255
|
-
const words = currentSentence.trimEnd().split(/\s+/);
|
|
258
|
+
const words = currentSentence.trimEnd().split(WHITESPACE_SPLIT_REGEX);
|
|
256
259
|
return words.length >= 2 ? words[words.length - 2] : '';
|
|
257
260
|
};
|
|
258
261
|
|
|
@@ -373,8 +376,7 @@ export const predict = textBefore => {
|
|
|
373
376
|
// }
|
|
374
377
|
|
|
375
378
|
// ── Step 2: Prefix completion (≥3 chars typed) ──────────────────────────
|
|
376
|
-
|
|
377
|
-
if (textBefore.length > 0 && /\s$/.test(textBefore)) {
|
|
379
|
+
if (textBefore.length > 0 && TRAILING_WHITESPACE_REGEX.test(textBefore)) {
|
|
378
380
|
return null;
|
|
379
381
|
}
|
|
380
382
|
const trimmed = textBefore.trimEnd();
|
|
@@ -31,6 +31,11 @@ var MAX_INGESTED_CONTEXT_TEXTS = 50;
|
|
|
31
31
|
// non-comment editors (where parentCommentContent never arrives) don't refetch
|
|
32
32
|
// on every word boundary for the plugin's lifetime.
|
|
33
33
|
var MAX_CONTEXT_REFRESH_ATTEMPTS = 5;
|
|
34
|
+
|
|
35
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
36
|
+
var TRAILING_NON_BOUNDARY_TOKEN_REGEX = /([^\s.,;:!?]*)$/;
|
|
37
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
38
|
+
var WORD_BOUNDARY_CHARS_REGEX = /[\s.,;:!?]/;
|
|
34
39
|
var hasDestroy = function hasDestroy(client) {
|
|
35
40
|
return 'destroy' in client && typeof client.destroy === 'function';
|
|
36
41
|
};
|
|
@@ -184,6 +189,8 @@ var buildSlowLaneText = function buildSlowLaneText(docText, context) {
|
|
|
184
189
|
var createMidWordCharacterRegex = function createMidWordCharacterRegex() {
|
|
185
190
|
try {
|
|
186
191
|
// string-built regex avoids TS1501 under the package's ES5 build target
|
|
192
|
+
// Ignored via go/ees019
|
|
193
|
+
// eslint-disable-next-line e18e/prefer-static-regex
|
|
187
194
|
return new RegExp('[\\p{L}\\p{N}_]', 'u');
|
|
188
195
|
} catch (_unused) {
|
|
189
196
|
return null;
|
|
@@ -218,9 +225,7 @@ var getTypedLengthForPrediction = function getTypedLengthForPrediction(textBefor
|
|
|
218
225
|
if (isWordBoundary(textBefore)) {
|
|
219
226
|
return 0;
|
|
220
227
|
}
|
|
221
|
-
|
|
222
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
223
|
-
var trailingToken = (_textBefore$match$ = (_textBefore$match = textBefore.match(/([^\s.,;:!?]*)$/)) === null || _textBefore$match === void 0 ? void 0 : _textBefore$match[1]) !== null && _textBefore$match$ !== void 0 ? _textBefore$match$ : '';
|
|
228
|
+
var trailingToken = (_textBefore$match$ = (_textBefore$match = textBefore.match(TRAILING_NON_BOUNDARY_TOKEN_REGEX)) === null || _textBefore$match === void 0 ? void 0 : _textBefore$match[1]) !== null && _textBefore$match$ !== void 0 ? _textBefore$match$ : '';
|
|
224
229
|
return trailingToken.length;
|
|
225
230
|
};
|
|
226
231
|
var isEnglishLocale = function isEnglishLocale(locale) {
|
|
@@ -532,16 +537,14 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
|
|
|
532
537
|
return;
|
|
533
538
|
}
|
|
534
539
|
var lastChar = newText[newText.length - 1];
|
|
535
|
-
|
|
536
|
-
if (!/[\s.,;:!?]/.test(lastChar)) {
|
|
540
|
+
if (!WORD_BOUNDARY_CHARS_REGEX.test(lastChar)) {
|
|
537
541
|
return;
|
|
538
542
|
}
|
|
539
543
|
|
|
540
544
|
// Only fire if the previous state did not already end on a boundary,
|
|
541
545
|
// so we don't double-count when multiple boundary chars are inserted.
|
|
542
546
|
var prevLastChar = prevText[prevText.length - 1];
|
|
543
|
-
|
|
544
|
-
if (prevLastChar && /[\s.,;:!?]/.test(prevLastChar)) {
|
|
547
|
+
if (prevLastChar && WORD_BOUNDARY_CHARS_REGEX.test(prevLastChar)) {
|
|
545
548
|
return;
|
|
546
549
|
}
|
|
547
550
|
var beforeBoundary = newText.slice(0, -1).trimEnd();
|
|
@@ -72,6 +72,8 @@ import { isWordBoundary } from './slow-lane-client';
|
|
|
72
72
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
73
73
|
|
|
74
74
|
var DEFAULT_DEBOUNCE_MS = 300;
|
|
75
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
76
|
+
var OOM_REGEX = /\boom\b/;
|
|
75
77
|
export var LOCAL_MLC_CAUSAL_MODEL_ID = 'SmolLM2-135M-Instruct-q0f16-MLC';
|
|
76
78
|
|
|
77
79
|
/**
|
|
@@ -756,9 +758,7 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
|
|
|
756
758
|
if (lower.includes('loading chunk') || lower.includes('dynamically imported module') || lower.includes('dynamic import')) {
|
|
757
759
|
return 'module_load_failed';
|
|
758
760
|
}
|
|
759
|
-
if (lower.includes('out of memory') ||
|
|
760
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
761
|
-
/\boom\b/.test(lower) || lower.includes('allocation') || lower.includes('exceeds') || lower.includes('buffer size') || lower.includes('not enough memory')) {
|
|
761
|
+
if (lower.includes('out of memory') || OOM_REGEX.test(lower) || lower.includes('allocation') || lower.includes('exceeds') || lower.includes('buffer size') || lower.includes('not enough memory')) {
|
|
762
762
|
return 'insufficient_memory';
|
|
763
763
|
}
|
|
764
764
|
// Pre-flight already returns missing_shader_f16 when the feature is absent,
|
|
@@ -23,6 +23,8 @@ import { isAutocompleteDebugEnabled } from './debug-mode';
|
|
|
23
23
|
|
|
24
24
|
// eslint-disable-next-line require-unicode-regexp
|
|
25
25
|
var WORD_BOUNDARY_CHARS = /[\s.,;:!?]/;
|
|
26
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
27
|
+
var TRAILING_SLASH_REGEX = /\/$/;
|
|
26
28
|
var DEFAULT_DEBOUNCE_MS = 300;
|
|
27
29
|
|
|
28
30
|
/**
|
|
@@ -74,8 +76,7 @@ export var createSlowLaneClient = function createSlowLaneClient(config) {
|
|
|
74
76
|
}
|
|
75
77
|
return _context.abrupt("return");
|
|
76
78
|
case 1:
|
|
77
|
-
|
|
78
|
-
url = "".concat(baseUrl.replace(/\/$/, '')).concat(endpoint);
|
|
79
|
+
url = "".concat(baseUrl.replace(TRAILING_SLASH_REGEX, '')).concat(endpoint);
|
|
79
80
|
payload = {
|
|
80
81
|
text: text,
|
|
81
82
|
session_id: sessionId
|
|
@@ -38,6 +38,12 @@ import { getStoredContextVector, getStoredLmLogits } from './slow-lane-client';
|
|
|
38
38
|
|
|
39
39
|
// eslint-disable-next-line require-unicode-regexp
|
|
40
40
|
var PUNCTUATION_BOUNDARY_REGEX = /^[.,;:!?()\[\]{}"'`]+|[.,;:!?()\[\]{}"'`]+$/g;
|
|
41
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
42
|
+
var WHITESPACE_SPLIT_REGEX = /\s+/;
|
|
43
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
44
|
+
var SENTENCE_BOUNDARY_REGEX = /[\n.?!]+/;
|
|
45
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
46
|
+
var TRAILING_WHITESPACE_REGEX = /\s$/;
|
|
41
47
|
var MIN_PREFIX_LENGTH = 3;
|
|
42
48
|
var MAX_CANDIDATES = 200;
|
|
43
49
|
var CONTEXT_WORDS = 10;
|
|
@@ -308,8 +314,8 @@ var getContextVectorForScoring = function getContextVectorForScoring(textBefore)
|
|
|
308
314
|
};
|
|
309
315
|
var tokenize = function tokenize(text) {
|
|
310
316
|
var tokens = [];
|
|
311
|
-
// eslint-disable-next-line
|
|
312
|
-
var _iterator7 = _createForOfIteratorHelper(text.toLowerCase().split(
|
|
317
|
+
// eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
|
|
318
|
+
var _iterator7 = _createForOfIteratorHelper(text.toLowerCase().split(WHITESPACE_SPLIT_REGEX)),
|
|
313
319
|
_step7;
|
|
314
320
|
try {
|
|
315
321
|
for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
|
|
@@ -329,12 +335,9 @@ var tokenize = function tokenize(text) {
|
|
|
329
335
|
};
|
|
330
336
|
var extractPreviousWord = function extractPreviousWord(text) {
|
|
331
337
|
// Only consider the current sentence/line the user is typing in.
|
|
332
|
-
|
|
333
|
-
var sentences = text.split(/[\n.?!]+/);
|
|
338
|
+
var sentences = text.split(SENTENCE_BOUNDARY_REGEX);
|
|
334
339
|
var currentSentence = sentences[sentences.length - 1];
|
|
335
|
-
|
|
336
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
337
|
-
var words = currentSentence.trimEnd().split(/\s+/);
|
|
340
|
+
var words = currentSentence.trimEnd().split(WHITESPACE_SPLIT_REGEX);
|
|
338
341
|
return words.length >= 2 ? words[words.length - 2] : '';
|
|
339
342
|
};
|
|
340
343
|
|
|
@@ -473,8 +476,7 @@ export var predict = function predict(textBefore) {
|
|
|
473
476
|
// }
|
|
474
477
|
|
|
475
478
|
// ── Step 2: Prefix completion (≥3 chars typed) ──────────────────────────
|
|
476
|
-
|
|
477
|
-
if (textBefore.length > 0 && /\s$/.test(textBefore)) {
|
|
479
|
+
if (textBefore.length > 0 && TRAILING_WHITESPACE_REGEX.test(textBefore)) {
|
|
478
480
|
return null;
|
|
479
481
|
}
|
|
480
482
|
var trimmed = textBefore.trimEnd();
|
|
@@ -40,17 +40,17 @@ export interface AutocompletePluginOptions {
|
|
|
40
40
|
* Use this to serve vectors from a CDN or media service in production.
|
|
41
41
|
*/
|
|
42
42
|
getVectorsBinaryUrl?: () => Promise<string>;
|
|
43
|
+
/**
|
|
44
|
+
* User locale used to determine whether autocomplete should run.
|
|
45
|
+
* Defaults to browser locale when omitted.
|
|
46
|
+
*/
|
|
47
|
+
locale?: string;
|
|
43
48
|
/**
|
|
44
49
|
* Subscription for hosts with live external context (e.g. Rovo chat). The plugin
|
|
45
50
|
* re-fetches via getContext() on each notification and unsubscribes on destroy.
|
|
46
51
|
* Only meaningful alongside `getContext`; without it each notification is a no-op.
|
|
47
52
|
*/
|
|
48
53
|
subscribeToContextUpdates?: (onContextUpdated: () => void) => () => void;
|
|
49
|
-
/**
|
|
50
|
-
* User locale used to determine whether autocomplete should run.
|
|
51
|
-
* Defaults to browser locale when omitted.
|
|
52
|
-
*/
|
|
53
|
-
locale?: string;
|
|
54
54
|
/**
|
|
55
55
|
* When true, uses on-device inference via WebGPU (MLC WebLLM) instead of
|
|
56
56
|
* the network-based slow-lane backend. Defaults to false (network client).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/editor-plugin-autocomplete",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.6",
|
|
4
4
|
"description": "Client-side text autocomplete plugin for @atlaskit/editor-core",
|
|
5
5
|
"author": "Atlassian Pty Ltd",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"wink-nlp": "^2.4.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@atlaskit/editor-common": "^116.
|
|
30
|
+
"@atlaskit/editor-common": "^116.21.0",
|
|
31
31
|
"@atlaskit/editor-plugin-analytics": "^12.0.0",
|
|
32
32
|
"react": "^18.2.0"
|
|
33
33
|
},
|
|
@@ -48,6 +48,11 @@ const MAX_INGESTED_CONTEXT_TEXTS = 50;
|
|
|
48
48
|
// on every word boundary for the plugin's lifetime.
|
|
49
49
|
const MAX_CONTEXT_REFRESH_ATTEMPTS = 5;
|
|
50
50
|
|
|
51
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
52
|
+
const TRAILING_NON_BOUNDARY_TOKEN_REGEX = /([^\s.,;:!?]*)$/;
|
|
53
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
54
|
+
const WORD_BOUNDARY_CHARS_REGEX = /[\s.,;:!?]/;
|
|
55
|
+
|
|
51
56
|
const hasDestroy = (
|
|
52
57
|
client: ReturnType<typeof createSlowLaneClient> | LocalSlowLaneClient,
|
|
53
58
|
): client is LocalSlowLaneClient => 'destroy' in client && typeof client.destroy === 'function';
|
|
@@ -214,17 +219,17 @@ export interface AutocompletePluginOptions {
|
|
|
214
219
|
* Use this to serve vectors from a CDN or media service in production.
|
|
215
220
|
*/
|
|
216
221
|
getVectorsBinaryUrl?: () => Promise<string>;
|
|
222
|
+
/**
|
|
223
|
+
* User locale used to determine whether autocomplete should run.
|
|
224
|
+
* Defaults to browser locale when omitted.
|
|
225
|
+
*/
|
|
226
|
+
locale?: string;
|
|
217
227
|
/**
|
|
218
228
|
* Subscription for hosts with live external context (e.g. Rovo chat). The plugin
|
|
219
229
|
* re-fetches via getContext() on each notification and unsubscribes on destroy.
|
|
220
230
|
* Only meaningful alongside `getContext`; without it each notification is a no-op.
|
|
221
231
|
*/
|
|
222
232
|
subscribeToContextUpdates?: (onContextUpdated: () => void) => () => void;
|
|
223
|
-
/**
|
|
224
|
-
* User locale used to determine whether autocomplete should run.
|
|
225
|
-
* Defaults to browser locale when omitted.
|
|
226
|
-
*/
|
|
227
|
-
locale?: string;
|
|
228
233
|
/**
|
|
229
234
|
* When true, uses on-device inference via WebGPU (MLC WebLLM) instead of
|
|
230
235
|
* the network-based slow-lane backend. Defaults to false (network client).
|
|
@@ -257,6 +262,8 @@ const buildSlowLaneText = (docText: string, context?: AutocompleteContext): stri
|
|
|
257
262
|
const createMidWordCharacterRegex = (): RegExp | null => {
|
|
258
263
|
try {
|
|
259
264
|
// string-built regex avoids TS1501 under the package's ES5 build target
|
|
265
|
+
// Ignored via go/ees019
|
|
266
|
+
// eslint-disable-next-line e18e/prefer-static-regex
|
|
260
267
|
return new RegExp('[\\p{L}\\p{N}_]', 'u');
|
|
261
268
|
} catch {
|
|
262
269
|
return null;
|
|
@@ -305,8 +312,7 @@ const getTypedLengthForPrediction = (textBefore: string): number => {
|
|
|
305
312
|
return 0;
|
|
306
313
|
}
|
|
307
314
|
|
|
308
|
-
|
|
309
|
-
const trailingToken = textBefore.match(/([^\s.,;:!?]*)$/)?.[1] ?? '';
|
|
315
|
+
const trailingToken = textBefore.match(TRAILING_NON_BOUNDARY_TOKEN_REGEX)?.[1] ?? '';
|
|
310
316
|
return trailingToken.length;
|
|
311
317
|
};
|
|
312
318
|
|
|
@@ -647,16 +653,14 @@ export const createAutocompletePlugin = (
|
|
|
647
653
|
}
|
|
648
654
|
|
|
649
655
|
const lastChar = newText[newText.length - 1];
|
|
650
|
-
|
|
651
|
-
if (!/[\s.,;:!?]/.test(lastChar)) {
|
|
656
|
+
if (!WORD_BOUNDARY_CHARS_REGEX.test(lastChar)) {
|
|
652
657
|
return;
|
|
653
658
|
}
|
|
654
659
|
|
|
655
660
|
// Only fire if the previous state did not already end on a boundary,
|
|
656
661
|
// so we don't double-count when multiple boundary chars are inserted.
|
|
657
662
|
const prevLastChar = prevText[prevText.length - 1];
|
|
658
|
-
|
|
659
|
-
if (prevLastChar && /[\s.,;:!?]/.test(prevLastChar)) {
|
|
663
|
+
if (prevLastChar && WORD_BOUNDARY_CHARS_REGEX.test(prevLastChar)) {
|
|
660
664
|
return;
|
|
661
665
|
}
|
|
662
666
|
|
|
@@ -174,6 +174,8 @@ interface MinimalGpu {
|
|
|
174
174
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
175
175
|
|
|
176
176
|
const DEFAULT_DEBOUNCE_MS = 300;
|
|
177
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
178
|
+
const OOM_REGEX = /\boom\b/;
|
|
177
179
|
|
|
178
180
|
export const LOCAL_MLC_CAUSAL_MODEL_ID = 'SmolLM2-135M-Instruct-q0f16-MLC';
|
|
179
181
|
|
|
@@ -769,8 +771,7 @@ export const createLocalSlowLaneClient = (
|
|
|
769
771
|
}
|
|
770
772
|
if (
|
|
771
773
|
lower.includes('out of memory') ||
|
|
772
|
-
|
|
773
|
-
/\boom\b/.test(lower) ||
|
|
774
|
+
OOM_REGEX.test(lower) ||
|
|
774
775
|
lower.includes('allocation') ||
|
|
775
776
|
lower.includes('exceeds') ||
|
|
776
777
|
lower.includes('buffer size') ||
|
|
@@ -29,6 +29,8 @@ export interface TypeaheadEncodingsResponse {
|
|
|
29
29
|
|
|
30
30
|
// eslint-disable-next-line require-unicode-regexp
|
|
31
31
|
const WORD_BOUNDARY_CHARS = /[\s.,;:!?]/;
|
|
32
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
33
|
+
const TRAILING_SLASH_REGEX = /\/$/;
|
|
32
34
|
const DEFAULT_DEBOUNCE_MS = 300;
|
|
33
35
|
|
|
34
36
|
/**
|
|
@@ -94,8 +96,7 @@ export const createSlowLaneClient = (
|
|
|
94
96
|
return;
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
|
|
98
|
-
const url = `${baseUrl.replace(/\/$/, '')}${endpoint}`;
|
|
99
|
+
const url = `${baseUrl.replace(TRAILING_SLASH_REGEX, '')}${endpoint}`;
|
|
99
100
|
const payload: TypeaheadEncodingsRequest = {
|
|
100
101
|
text,
|
|
101
102
|
session_id: sessionId,
|
|
@@ -29,6 +29,12 @@ import { getStoredContextVector, getStoredLmLogits } from './slow-lane-client';
|
|
|
29
29
|
|
|
30
30
|
// eslint-disable-next-line require-unicode-regexp
|
|
31
31
|
const PUNCTUATION_BOUNDARY_REGEX = /^[.,;:!?()\[\]{}"'`]+|[.,;:!?()\[\]{}"'`]+$/g;
|
|
32
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
33
|
+
const WHITESPACE_SPLIT_REGEX = /\s+/;
|
|
34
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
35
|
+
const SENTENCE_BOUNDARY_REGEX = /[\n.?!]+/;
|
|
36
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
37
|
+
const TRAILING_WHITESPACE_REGEX = /\s$/;
|
|
32
38
|
|
|
33
39
|
const MIN_PREFIX_LENGTH = 3;
|
|
34
40
|
const MAX_CANDIDATES = 200;
|
|
@@ -275,8 +281,8 @@ const getContextVectorForScoring = (textBefore: string): Float32Array | null =>
|
|
|
275
281
|
|
|
276
282
|
const tokenize = (text: string): string[] => {
|
|
277
283
|
const tokens: string[] = [];
|
|
278
|
-
// eslint-disable-next-line
|
|
279
|
-
for (const raw of text.toLowerCase().split(
|
|
284
|
+
// eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
|
|
285
|
+
for (const raw of text.toLowerCase().split(WHITESPACE_SPLIT_REGEX)) {
|
|
280
286
|
// eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
|
|
281
287
|
const clean = raw.replace(PUNCTUATION_BOUNDARY_REGEX, '');
|
|
282
288
|
if (clean.length >= 2) {
|
|
@@ -288,12 +294,10 @@ const tokenize = (text: string): string[] => {
|
|
|
288
294
|
|
|
289
295
|
const extractPreviousWord = (text: string): string => {
|
|
290
296
|
// Only consider the current sentence/line the user is typing in.
|
|
291
|
-
|
|
292
|
-
const sentences = text.split(/[\n.?!]+/);
|
|
297
|
+
const sentences = text.split(SENTENCE_BOUNDARY_REGEX);
|
|
293
298
|
const currentSentence = sentences[sentences.length - 1];
|
|
294
299
|
|
|
295
|
-
|
|
296
|
-
const words = currentSentence.trimEnd().split(/\s+/);
|
|
300
|
+
const words = currentSentence.trimEnd().split(WHITESPACE_SPLIT_REGEX);
|
|
297
301
|
return words.length >= 2 ? words[words.length - 2] : '';
|
|
298
302
|
};
|
|
299
303
|
|
|
@@ -442,8 +446,7 @@ export const predict = (textBefore: string): string | null => {
|
|
|
442
446
|
// }
|
|
443
447
|
|
|
444
448
|
// ── Step 2: Prefix completion (≥3 chars typed) ──────────────────────────
|
|
445
|
-
|
|
446
|
-
if (textBefore.length > 0 && /\s$/.test(textBefore)) {
|
|
449
|
+
if (textBefore.length > 0 && TRAILING_WHITESPACE_REGEX.test(textBefore)) {
|
|
447
450
|
return null;
|
|
448
451
|
}
|
|
449
452
|
|