@atlaskit/editor-plugin-autocomplete 4.0.0 → 4.0.2
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 +18 -0
- package/dist/cjs/pm-plugins/autocomplete-plugin.js +63 -0
- package/dist/es2019/pm-plugins/autocomplete-plugin.js +63 -0
- package/dist/esm/pm-plugins/autocomplete-plugin.js +63 -0
- package/dist/types/pm-plugins/autocomplete-plugin.d.ts +5 -0
- package/package.json +2 -2
- package/src/pm-plugins/autocomplete-plugin.ts +88 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @atlaskit/editor-plugin-autocomplete
|
|
2
2
|
|
|
3
|
+
## 4.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`56263a9d5bb6d`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/56263a9d5bb6d) -
|
|
8
|
+
Gate editor autocomplete to English locales so suggestions and model loading are disabled for
|
|
9
|
+
non-English users.
|
|
10
|
+
|
|
11
|
+
## 4.0.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [`560ddb7a914f6`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/560ddb7a914f6) -
|
|
16
|
+
[ux] Fixes a bug where autocomplete suggestions were shown when the cursor was positioned in the
|
|
17
|
+
middle of an existing word. Suggestions are now suppressed unless the cursor is at the trailing
|
|
18
|
+
edge of a token.
|
|
19
|
+
- Updated dependencies
|
|
20
|
+
|
|
3
21
|
## 4.0.0
|
|
4
22
|
|
|
5
23
|
### Major Changes
|
|
@@ -188,6 +188,38 @@ var buildSlowLaneText = function buildSlowLaneText(docText, context) {
|
|
|
188
188
|
lines.push("reply ".concat(nextReplyNumber, ": ").concat(docText));
|
|
189
189
|
return lines.join('\n');
|
|
190
190
|
};
|
|
191
|
+
var createMidWordCharacterRegex = function createMidWordCharacterRegex() {
|
|
192
|
+
try {
|
|
193
|
+
// string-built regex avoids TS1501 under the package's ES5 build target
|
|
194
|
+
return new RegExp('[\\p{L}\\p{N}_]', 'u');
|
|
195
|
+
} catch (_unused) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
var midWordCharacterRegex = createMidWordCharacterRegex();
|
|
200
|
+
var isAsciiWordCharacter = function isAsciiWordCharacter(char) {
|
|
201
|
+
if (char === '_') {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
var charCode = char.charCodeAt(0);
|
|
205
|
+
return charCode >= 48 && charCode <= 57 || charCode >= 65 && charCode <= 90 || charCode >= 97 && charCode <= 122;
|
|
206
|
+
};
|
|
207
|
+
var isMidWordCharacter = function isMidWordCharacter(char) {
|
|
208
|
+
if (midWordCharacterRegex) {
|
|
209
|
+
return midWordCharacterRegex.test(char);
|
|
210
|
+
}
|
|
211
|
+
if (isAsciiWordCharacter(char)) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
return char.toLocaleLowerCase() !== char.toLocaleUpperCase();
|
|
215
|
+
};
|
|
216
|
+
var getLeadingTextCharacter = function getLeadingTextCharacter(text) {
|
|
217
|
+
var firstCodePoint = text === null || text === void 0 ? void 0 : text.codePointAt(0);
|
|
218
|
+
if (firstCodePoint === undefined) {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
return String.fromCodePoint(firstCodePoint);
|
|
222
|
+
};
|
|
191
223
|
var getTypedLengthForPrediction = function getTypedLengthForPrediction(textBefore) {
|
|
192
224
|
var _textBefore$match$, _textBefore$match;
|
|
193
225
|
if ((0, _slowLaneClient.isWordBoundary)(textBefore)) {
|
|
@@ -198,7 +230,16 @@ var getTypedLengthForPrediction = function getTypedLengthForPrediction(textBefor
|
|
|
198
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$ : '';
|
|
199
231
|
return trailingToken.length;
|
|
200
232
|
};
|
|
233
|
+
var isEnglishLocale = function isEnglishLocale(locale) {
|
|
234
|
+
if (!locale) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
return locale.toLowerCase().startsWith('en');
|
|
238
|
+
};
|
|
201
239
|
var createAutocompletePlugin = exports.createAutocompletePlugin = function createAutocompletePlugin(options, api) {
|
|
240
|
+
var _options$locale;
|
|
241
|
+
var locale = (_options$locale = options === null || options === void 0 ? void 0 : options.locale) !== null && _options$locale !== void 0 ? _options$locale : typeof navigator !== 'undefined' ? navigator.language : undefined;
|
|
242
|
+
var isAutocompleteEnabled = isEnglishLocale(locale);
|
|
202
243
|
var debounceTimer = null;
|
|
203
244
|
var hasIngestedPage = false;
|
|
204
245
|
var resolvedContext;
|
|
@@ -440,6 +481,19 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
|
|
|
440
481
|
if (!selection.empty) {
|
|
441
482
|
return;
|
|
442
483
|
}
|
|
484
|
+
|
|
485
|
+
// Suppress suggestions when the cursor is mid-word — only offer
|
|
486
|
+
// completions when the cursor is at the trailing edge of a token.
|
|
487
|
+
// nodeAfter correctly handles inline atoms (mentions, emojis) where
|
|
488
|
+
// parentOffset and textContent indices diverge.
|
|
489
|
+
var nodeAfter = selection.$from.nodeAfter;
|
|
490
|
+
var charAfterCursor = nodeAfter !== null && nodeAfter !== void 0 && nodeAfter.isText ? getLeadingTextCharacter(nodeAfter.text) : undefined;
|
|
491
|
+
// Only suppress for Unicode letters, digits, and underscore — hyphens,
|
|
492
|
+
// apostrophes, and similar punctuation are valid left-edge boundaries
|
|
493
|
+
// and should not block suggestions (e.g. cursor before '-' in compound-word).
|
|
494
|
+
if (charAfterCursor && isMidWordCharacter(charAfterCursor)) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
443
497
|
var textBefore = getTextBeforeCursor(state);
|
|
444
498
|
|
|
445
499
|
// Suppress re-showing the same suggestion the user just dismissed.
|
|
@@ -575,6 +629,9 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
|
|
|
575
629
|
}),
|
|
576
630
|
handleDOMEvents: {
|
|
577
631
|
blur: function blur(view) {
|
|
632
|
+
if (!isAutocompleteEnabled) {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
578
635
|
var pluginState = autocompletePluginKey.getState(view.state);
|
|
579
636
|
if (pluginState !== null && pluginState !== void 0 && pluginState.ghostText) {
|
|
580
637
|
clearGhostText(view.state, view.dispatch);
|
|
@@ -584,6 +641,9 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
|
|
|
584
641
|
},
|
|
585
642
|
focus: function focus() {
|
|
586
643
|
var _options$useLocalMode, _options$useLocalMode2;
|
|
644
|
+
if (!isAutocompleteEnabled) {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
587
647
|
(0, _textPredictor.loadDefaultVocabulary)({
|
|
588
648
|
isLocalLLM: (_options$useLocalMode = options === null || options === void 0 ? void 0 : options.useLocalModel) !== null && _options$useLocalMode !== void 0 ? _options$useLocalMode : false
|
|
589
649
|
}).catch(function (error) {
|
|
@@ -613,6 +673,9 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
|
|
|
613
673
|
view: function view() {
|
|
614
674
|
return {
|
|
615
675
|
update: function update(view, prevState) {
|
|
676
|
+
if (!isAutocompleteEnabled) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
616
679
|
currentView = view;
|
|
617
680
|
if (!prevState.doc.eq(view.state.doc)) {
|
|
618
681
|
if (justAccepted) {
|
|
@@ -176,6 +176,38 @@ const buildSlowLaneText = (docText, context) => {
|
|
|
176
176
|
lines.push(`reply ${nextReplyNumber}: ${docText}`);
|
|
177
177
|
return lines.join('\n');
|
|
178
178
|
};
|
|
179
|
+
const createMidWordCharacterRegex = () => {
|
|
180
|
+
try {
|
|
181
|
+
// string-built regex avoids TS1501 under the package's ES5 build target
|
|
182
|
+
return new RegExp('[\\p{L}\\p{N}_]', 'u');
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
const midWordCharacterRegex = createMidWordCharacterRegex();
|
|
188
|
+
const isAsciiWordCharacter = char => {
|
|
189
|
+
if (char === '_') {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
const charCode = char.charCodeAt(0);
|
|
193
|
+
return charCode >= 48 && charCode <= 57 || charCode >= 65 && charCode <= 90 || charCode >= 97 && charCode <= 122;
|
|
194
|
+
};
|
|
195
|
+
const isMidWordCharacter = char => {
|
|
196
|
+
if (midWordCharacterRegex) {
|
|
197
|
+
return midWordCharacterRegex.test(char);
|
|
198
|
+
}
|
|
199
|
+
if (isAsciiWordCharacter(char)) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
return char.toLocaleLowerCase() !== char.toLocaleUpperCase();
|
|
203
|
+
};
|
|
204
|
+
const getLeadingTextCharacter = text => {
|
|
205
|
+
const firstCodePoint = text === null || text === void 0 ? void 0 : text.codePointAt(0);
|
|
206
|
+
if (firstCodePoint === undefined) {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
return String.fromCodePoint(firstCodePoint);
|
|
210
|
+
};
|
|
179
211
|
const getTypedLengthForPrediction = textBefore => {
|
|
180
212
|
var _textBefore$match$, _textBefore$match;
|
|
181
213
|
if (isWordBoundary(textBefore)) {
|
|
@@ -186,7 +218,16 @@ const getTypedLengthForPrediction = textBefore => {
|
|
|
186
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$ : '';
|
|
187
219
|
return trailingToken.length;
|
|
188
220
|
};
|
|
221
|
+
const isEnglishLocale = locale => {
|
|
222
|
+
if (!locale) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
return locale.toLowerCase().startsWith('en');
|
|
226
|
+
};
|
|
189
227
|
export const createAutocompletePlugin = (options, api) => {
|
|
228
|
+
var _options$locale;
|
|
229
|
+
const locale = (_options$locale = options === null || options === void 0 ? void 0 : options.locale) !== null && _options$locale !== void 0 ? _options$locale : typeof navigator !== 'undefined' ? navigator.language : undefined;
|
|
230
|
+
const isAutocompleteEnabled = isEnglishLocale(locale);
|
|
190
231
|
let debounceTimer = null;
|
|
191
232
|
let hasIngestedPage = false;
|
|
192
233
|
let resolvedContext;
|
|
@@ -424,6 +465,19 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
424
465
|
if (!selection.empty) {
|
|
425
466
|
return;
|
|
426
467
|
}
|
|
468
|
+
|
|
469
|
+
// Suppress suggestions when the cursor is mid-word — only offer
|
|
470
|
+
// completions when the cursor is at the trailing edge of a token.
|
|
471
|
+
// nodeAfter correctly handles inline atoms (mentions, emojis) where
|
|
472
|
+
// parentOffset and textContent indices diverge.
|
|
473
|
+
const nodeAfter = selection.$from.nodeAfter;
|
|
474
|
+
const charAfterCursor = nodeAfter !== null && nodeAfter !== void 0 && nodeAfter.isText ? getLeadingTextCharacter(nodeAfter.text) : undefined;
|
|
475
|
+
// Only suppress for Unicode letters, digits, and underscore — hyphens,
|
|
476
|
+
// apostrophes, and similar punctuation are valid left-edge boundaries
|
|
477
|
+
// and should not block suggestions (e.g. cursor before '-' in compound-word).
|
|
478
|
+
if (charAfterCursor && isMidWordCharacter(charAfterCursor)) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
427
481
|
const textBefore = getTextBeforeCursor(state);
|
|
428
482
|
|
|
429
483
|
// Suppress re-showing the same suggestion the user just dismissed.
|
|
@@ -562,6 +616,9 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
562
616
|
}),
|
|
563
617
|
handleDOMEvents: {
|
|
564
618
|
blur: view => {
|
|
619
|
+
if (!isAutocompleteEnabled) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
565
622
|
const pluginState = autocompletePluginKey.getState(view.state);
|
|
566
623
|
if (pluginState !== null && pluginState !== void 0 && pluginState.ghostText) {
|
|
567
624
|
clearGhostText(view.state, view.dispatch);
|
|
@@ -571,6 +628,9 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
571
628
|
},
|
|
572
629
|
focus: () => {
|
|
573
630
|
var _options$useLocalMode, _options$useLocalMode2;
|
|
631
|
+
if (!isAutocompleteEnabled) {
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
574
634
|
loadDefaultVocabulary({
|
|
575
635
|
isLocalLLM: (_options$useLocalMode = options === null || options === void 0 ? void 0 : options.useLocalModel) !== null && _options$useLocalMode !== void 0 ? _options$useLocalMode : false
|
|
576
636
|
}).catch(error => {
|
|
@@ -599,6 +659,9 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
599
659
|
},
|
|
600
660
|
view: () => ({
|
|
601
661
|
update: (view, prevState) => {
|
|
662
|
+
if (!isAutocompleteEnabled) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
602
665
|
currentView = view;
|
|
603
666
|
if (!prevState.doc.eq(view.state.doc)) {
|
|
604
667
|
if (justAccepted) {
|
|
@@ -181,6 +181,38 @@ var buildSlowLaneText = function buildSlowLaneText(docText, context) {
|
|
|
181
181
|
lines.push("reply ".concat(nextReplyNumber, ": ").concat(docText));
|
|
182
182
|
return lines.join('\n');
|
|
183
183
|
};
|
|
184
|
+
var createMidWordCharacterRegex = function createMidWordCharacterRegex() {
|
|
185
|
+
try {
|
|
186
|
+
// string-built regex avoids TS1501 under the package's ES5 build target
|
|
187
|
+
return new RegExp('[\\p{L}\\p{N}_]', 'u');
|
|
188
|
+
} catch (_unused) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
var midWordCharacterRegex = createMidWordCharacterRegex();
|
|
193
|
+
var isAsciiWordCharacter = function isAsciiWordCharacter(char) {
|
|
194
|
+
if (char === '_') {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
var charCode = char.charCodeAt(0);
|
|
198
|
+
return charCode >= 48 && charCode <= 57 || charCode >= 65 && charCode <= 90 || charCode >= 97 && charCode <= 122;
|
|
199
|
+
};
|
|
200
|
+
var isMidWordCharacter = function isMidWordCharacter(char) {
|
|
201
|
+
if (midWordCharacterRegex) {
|
|
202
|
+
return midWordCharacterRegex.test(char);
|
|
203
|
+
}
|
|
204
|
+
if (isAsciiWordCharacter(char)) {
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
return char.toLocaleLowerCase() !== char.toLocaleUpperCase();
|
|
208
|
+
};
|
|
209
|
+
var getLeadingTextCharacter = function getLeadingTextCharacter(text) {
|
|
210
|
+
var firstCodePoint = text === null || text === void 0 ? void 0 : text.codePointAt(0);
|
|
211
|
+
if (firstCodePoint === undefined) {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
return String.fromCodePoint(firstCodePoint);
|
|
215
|
+
};
|
|
184
216
|
var getTypedLengthForPrediction = function getTypedLengthForPrediction(textBefore) {
|
|
185
217
|
var _textBefore$match$, _textBefore$match;
|
|
186
218
|
if (isWordBoundary(textBefore)) {
|
|
@@ -191,7 +223,16 @@ var getTypedLengthForPrediction = function getTypedLengthForPrediction(textBefor
|
|
|
191
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$ : '';
|
|
192
224
|
return trailingToken.length;
|
|
193
225
|
};
|
|
226
|
+
var isEnglishLocale = function isEnglishLocale(locale) {
|
|
227
|
+
if (!locale) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
return locale.toLowerCase().startsWith('en');
|
|
231
|
+
};
|
|
194
232
|
export var createAutocompletePlugin = function createAutocompletePlugin(options, api) {
|
|
233
|
+
var _options$locale;
|
|
234
|
+
var locale = (_options$locale = options === null || options === void 0 ? void 0 : options.locale) !== null && _options$locale !== void 0 ? _options$locale : typeof navigator !== 'undefined' ? navigator.language : undefined;
|
|
235
|
+
var isAutocompleteEnabled = isEnglishLocale(locale);
|
|
195
236
|
var debounceTimer = null;
|
|
196
237
|
var hasIngestedPage = false;
|
|
197
238
|
var resolvedContext;
|
|
@@ -433,6 +474,19 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
|
|
|
433
474
|
if (!selection.empty) {
|
|
434
475
|
return;
|
|
435
476
|
}
|
|
477
|
+
|
|
478
|
+
// Suppress suggestions when the cursor is mid-word — only offer
|
|
479
|
+
// completions when the cursor is at the trailing edge of a token.
|
|
480
|
+
// nodeAfter correctly handles inline atoms (mentions, emojis) where
|
|
481
|
+
// parentOffset and textContent indices diverge.
|
|
482
|
+
var nodeAfter = selection.$from.nodeAfter;
|
|
483
|
+
var charAfterCursor = nodeAfter !== null && nodeAfter !== void 0 && nodeAfter.isText ? getLeadingTextCharacter(nodeAfter.text) : undefined;
|
|
484
|
+
// Only suppress for Unicode letters, digits, and underscore — hyphens,
|
|
485
|
+
// apostrophes, and similar punctuation are valid left-edge boundaries
|
|
486
|
+
// and should not block suggestions (e.g. cursor before '-' in compound-word).
|
|
487
|
+
if (charAfterCursor && isMidWordCharacter(charAfterCursor)) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
436
490
|
var textBefore = getTextBeforeCursor(state);
|
|
437
491
|
|
|
438
492
|
// Suppress re-showing the same suggestion the user just dismissed.
|
|
@@ -568,6 +622,9 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
|
|
|
568
622
|
}),
|
|
569
623
|
handleDOMEvents: {
|
|
570
624
|
blur: function blur(view) {
|
|
625
|
+
if (!isAutocompleteEnabled) {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
571
628
|
var pluginState = autocompletePluginKey.getState(view.state);
|
|
572
629
|
if (pluginState !== null && pluginState !== void 0 && pluginState.ghostText) {
|
|
573
630
|
clearGhostText(view.state, view.dispatch);
|
|
@@ -577,6 +634,9 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
|
|
|
577
634
|
},
|
|
578
635
|
focus: function focus() {
|
|
579
636
|
var _options$useLocalMode, _options$useLocalMode2;
|
|
637
|
+
if (!isAutocompleteEnabled) {
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
580
640
|
loadDefaultVocabulary({
|
|
581
641
|
isLocalLLM: (_options$useLocalMode = options === null || options === void 0 ? void 0 : options.useLocalModel) !== null && _options$useLocalMode !== void 0 ? _options$useLocalMode : false
|
|
582
642
|
}).catch(function (error) {
|
|
@@ -606,6 +666,9 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
|
|
|
606
666
|
view: function view() {
|
|
607
667
|
return {
|
|
608
668
|
update: function update(view, prevState) {
|
|
669
|
+
if (!isAutocompleteEnabled) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
609
672
|
currentView = view;
|
|
610
673
|
if (!prevState.doc.eq(view.state.doc)) {
|
|
611
674
|
if (justAccepted) {
|
|
@@ -40,6 +40,11 @@ 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
|
* When true, uses on-device inference via WebGPU (MLC WebLLM) instead of
|
|
45
50
|
* 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.2",
|
|
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.4.0",
|
|
31
31
|
"@atlaskit/editor-plugin-analytics": "^12.0.0",
|
|
32
32
|
"react": "^18.2.0"
|
|
33
33
|
},
|
|
@@ -214,6 +214,11 @@ export interface AutocompletePluginOptions {
|
|
|
214
214
|
* Use this to serve vectors from a CDN or media service in production.
|
|
215
215
|
*/
|
|
216
216
|
getVectorsBinaryUrl?: () => Promise<string>;
|
|
217
|
+
/**
|
|
218
|
+
* User locale used to determine whether autocomplete should run.
|
|
219
|
+
* Defaults to browser locale when omitted.
|
|
220
|
+
*/
|
|
221
|
+
locale?: string;
|
|
217
222
|
/**
|
|
218
223
|
* When true, uses on-device inference via WebGPU (MLC WebLLM) instead of
|
|
219
224
|
* the network-based slow-lane backend. Defaults to false (network client).
|
|
@@ -243,6 +248,52 @@ const buildSlowLaneText = (docText: string, context?: AutocompleteContext): stri
|
|
|
243
248
|
return lines.join('\n');
|
|
244
249
|
};
|
|
245
250
|
|
|
251
|
+
const createMidWordCharacterRegex = (): RegExp | null => {
|
|
252
|
+
try {
|
|
253
|
+
// string-built regex avoids TS1501 under the package's ES5 build target
|
|
254
|
+
return new RegExp('[\\p{L}\\p{N}_]', 'u');
|
|
255
|
+
} catch {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const midWordCharacterRegex = createMidWordCharacterRegex();
|
|
261
|
+
|
|
262
|
+
const isAsciiWordCharacter = (char: string): boolean => {
|
|
263
|
+
if (char === '_') {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const charCode = char.charCodeAt(0);
|
|
268
|
+
return (
|
|
269
|
+
(charCode >= 48 && charCode <= 57) ||
|
|
270
|
+
(charCode >= 65 && charCode <= 90) ||
|
|
271
|
+
(charCode >= 97 && charCode <= 122)
|
|
272
|
+
);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const isMidWordCharacter = (char: string): boolean => {
|
|
276
|
+
if (midWordCharacterRegex) {
|
|
277
|
+
return midWordCharacterRegex.test(char);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (isAsciiWordCharacter(char)) {
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return char.toLocaleLowerCase() !== char.toLocaleUpperCase();
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const getLeadingTextCharacter = (text?: string): string | undefined => {
|
|
288
|
+
const firstCodePoint = text?.codePointAt(0);
|
|
289
|
+
|
|
290
|
+
if (firstCodePoint === undefined) {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return String.fromCodePoint(firstCodePoint);
|
|
295
|
+
};
|
|
296
|
+
|
|
246
297
|
const getTypedLengthForPrediction = (textBefore: string): number => {
|
|
247
298
|
if (isWordBoundary(textBefore)) {
|
|
248
299
|
return 0;
|
|
@@ -253,10 +304,22 @@ const getTypedLengthForPrediction = (textBefore: string): number => {
|
|
|
253
304
|
return trailingToken.length;
|
|
254
305
|
};
|
|
255
306
|
|
|
307
|
+
const isEnglishLocale = (locale?: string): boolean => {
|
|
308
|
+
if (!locale) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return locale.toLowerCase().startsWith('en');
|
|
313
|
+
};
|
|
314
|
+
|
|
256
315
|
export const createAutocompletePlugin = (
|
|
257
316
|
options?: AutocompletePluginOptions,
|
|
258
317
|
api?: ExtractInjectionAPI<AutocompletePlugin>,
|
|
259
318
|
): SafePlugin<AutocompletePluginState> => {
|
|
319
|
+
const locale =
|
|
320
|
+
options?.locale ?? (typeof navigator !== 'undefined' ? navigator.language : undefined);
|
|
321
|
+
const isAutocompleteEnabled = isEnglishLocale(locale);
|
|
322
|
+
|
|
260
323
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
261
324
|
let hasIngestedPage = false;
|
|
262
325
|
let resolvedContext: AutocompleteContext | undefined;
|
|
@@ -514,6 +577,19 @@ export const createAutocompletePlugin = (
|
|
|
514
577
|
return;
|
|
515
578
|
}
|
|
516
579
|
|
|
580
|
+
// Suppress suggestions when the cursor is mid-word — only offer
|
|
581
|
+
// completions when the cursor is at the trailing edge of a token.
|
|
582
|
+
// nodeAfter correctly handles inline atoms (mentions, emojis) where
|
|
583
|
+
// parentOffset and textContent indices diverge.
|
|
584
|
+
const nodeAfter = selection.$from.nodeAfter;
|
|
585
|
+
const charAfterCursor = nodeAfter?.isText ? getLeadingTextCharacter(nodeAfter.text) : undefined;
|
|
586
|
+
// Only suppress for Unicode letters, digits, and underscore — hyphens,
|
|
587
|
+
// apostrophes, and similar punctuation are valid left-edge boundaries
|
|
588
|
+
// and should not block suggestions (e.g. cursor before '-' in compound-word).
|
|
589
|
+
if (charAfterCursor && isMidWordCharacter(charAfterCursor)) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
517
593
|
const textBefore = getTextBeforeCursor(state);
|
|
518
594
|
|
|
519
595
|
// Suppress re-showing the same suggestion the user just dismissed.
|
|
@@ -671,6 +747,10 @@ export const createAutocompletePlugin = (
|
|
|
671
747
|
|
|
672
748
|
handleDOMEvents: {
|
|
673
749
|
blur: (view: EditorView) => {
|
|
750
|
+
if (!isAutocompleteEnabled) {
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
|
|
674
754
|
const pluginState = autocompletePluginKey.getState(view.state) as
|
|
675
755
|
| AutocompletePluginState
|
|
676
756
|
| undefined;
|
|
@@ -681,6 +761,10 @@ export const createAutocompletePlugin = (
|
|
|
681
761
|
return false;
|
|
682
762
|
},
|
|
683
763
|
focus: () => {
|
|
764
|
+
if (!isAutocompleteEnabled) {
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
|
|
684
768
|
loadDefaultVocabulary({ isLocalLLM: options?.useLocalModel ?? false }).catch((error) => {
|
|
685
769
|
logException(error as Error, {
|
|
686
770
|
location: 'editor-plugin-autocomplete/loadDefaultVocabulary',
|
|
@@ -708,6 +792,10 @@ export const createAutocompletePlugin = (
|
|
|
708
792
|
|
|
709
793
|
view: () => ({
|
|
710
794
|
update: (view: EditorView, prevState: EditorState) => {
|
|
795
|
+
if (!isAutocompleteEnabled) {
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
|
|
711
799
|
currentView = view;
|
|
712
800
|
if (!prevState.doc.eq(view.state.doc)) {
|
|
713
801
|
if (justAccepted) {
|