@atlaskit/editor-plugin-autocomplete 3.0.0 → 3.1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @atlaskit/editor-plugin-autocomplete
2
2
 
3
+ ## 3.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`f003833231999`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/f003833231999) -
8
+ Code-split the autocomplete vocabulary, L3 word list and word-index JSON via dynamic import so
9
+ they load on autocomplete initialisation instead of being bundled into the editor's main chunk.
10
+
3
11
  ## 3.0.0
4
12
 
5
13
  ### Patch Changes
@@ -38,8 +38,6 @@ var createInitialState = function createInitialState() {
38
38
  var getTextBeforeCursor = function getTextBeforeCursor(state) {
39
39
  var $from = state.selection.$from;
40
40
  var maxChars = 200;
41
-
42
- // 1. Get the perfectly flattened text of the current block up to the cursor
43
41
  var blockNode = $from.parent;
44
42
  var offsetInBlock = $from.parentOffset;
45
43
  var blockText = blockNode.textContent.slice(0, offsetInBlock);
@@ -48,7 +46,7 @@ var getTextBeforeCursor = function getTextBeforeCursor(state) {
48
46
  }
49
47
  var fullText = blockText;
50
48
 
51
- // 2. Walk backwards through previous blocks
49
+ // Walk backwards through previous blocks until we have enough context.
52
50
  var depth = $from.depth - 1;
53
51
  while (fullText.length < maxChars && depth >= 0) {
54
52
  var parentNode = $from.node(depth);
@@ -249,8 +247,6 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
249
247
  try {
250
248
  var state = view.state;
251
249
  var selection = state.selection;
252
-
253
- // Only predict for cursor selections (not range selections)
254
250
  if (!selection.empty) {
255
251
  return;
256
252
  }
@@ -262,13 +258,9 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
262
258
  return;
263
259
  }
264
260
  dismissedContext = null;
265
-
266
- // Don't predict if there's not enough context
267
261
  if (textBefore.trim().length < 3) {
268
262
  return;
269
263
  }
270
-
271
- // Tier 1 prediction is synchronous -- no async needed
272
264
  var prediction = (0, _textPredictor.predict)(textBefore);
273
265
  if (prediction && prediction.length > 0) {
274
266
  var typedLength = getTypedLengthForPrediction(textBefore);
@@ -330,8 +322,7 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
330
322
  return _objectSpread(_objectSpread({}, pluginState), meta);
331
323
  }
332
324
 
333
- // If the document changed, clear the ghost text
334
- // (new prediction will be scheduled from view.update)
325
+ // A new prediction is scheduled from view.update.
335
326
  if (tr.docChanged) {
336
327
  return _objectSpread(_objectSpread({}, pluginState), {}, {
337
328
  ghostText: '',
@@ -339,8 +330,6 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
339
330
  decorationSet: _view.DecorationSet.empty
340
331
  });
341
332
  }
342
-
343
- // If selection changed without doc change, clear ghost text
344
333
  if (tr.selectionSet && pluginState.ghostText) {
345
334
  return _objectSpread(_objectSpread({}, pluginState), {}, {
346
335
  ghostText: '',
@@ -401,13 +390,11 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
401
390
  return false;
402
391
  },
403
392
  focus: function focus() {
404
- try {
405
- (0, _textPredictor.loadDefaultVocabulary)();
406
- } catch (error) {
393
+ (0, _textPredictor.loadDefaultVocabulary)().catch(function (error) {
407
394
  (0, _monitoring.logException)(error, {
408
395
  location: 'editor-plugin-autocomplete/loadDefaultVocabulary'
409
396
  });
410
- }
397
+ });
411
398
  (0, _textPredictor.loadVectorsAsync)({
412
399
  getBinaryUrl: options === null || options === void 0 ? void 0 : options.getVectorsBinaryUrl
413
400
  }).catch(function (error) {
@@ -449,12 +436,9 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
449
436
  if (justAccepted) {
450
437
  justAccepted = false;
451
438
 
452
- // ✨ THE FIX: Memorize the text state right after acceptance.
453
- // Any follow-up transactions will hit the 'dismissedContext'
454
- // block and abort until the user actually types a new character!
439
+ // Snapshot the post-acceptance text so follow-up transactions hit
440
+ // the dismissedContext guard and abort until the user types again.
455
441
  dismissedContext = getTextBeforeCursor(view.state);
456
-
457
- // Also clear any pending debounce timers from before the acceptance
458
442
  if (debounceTimer) {
459
443
  clearTimeout(debounceTimer);
460
444
  }
@@ -166,20 +166,21 @@ function applyGrammarFilter(candidates, previousWord) {
166
166
  dropped.push(entry.candidate.word);
167
167
  }
168
168
  }
169
+
170
+ // Grammar is authoritative.
169
171
  } catch (err) {
170
172
  _iterator2.e(err);
171
173
  } finally {
172
174
  _iterator2.f();
173
175
  }
174
- var finalFiltered = filtered.length > 0 ? filtered : candidates;
175
176
  return {
176
- filtered: finalFiltered,
177
+ filtered: filtered,
177
178
  grammarMeta: {
178
179
  prevWord: lowerPrev,
179
180
  prevTags: prevTags,
180
181
  before: candidates.length,
181
- after: finalFiltered.length,
182
- dropped: filtered.length > 0 ? dropped : []
182
+ after: filtered.length,
183
+ dropped: dropped
183
184
  }
184
185
  };
185
186
  }
@@ -1,23 +1,23 @@
1
1
  "use strict";
2
2
 
3
3
  var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ var _typeof3 = require("@babel/runtime/helpers/typeof");
4
5
  Object.defineProperty(exports, "__esModule", {
5
6
  value: true
6
7
  });
7
8
  exports.predict = exports.loadVectorsAsync = exports.loadDefaultVocabulary = exports.initVocabulary = exports.initVectors = exports.initL3Vocabulary = exports.ingestDocumentPage = exports.incrementSessionFreq = exports.getPredictorStatus = exports.getLastPredictionDebug = void 0;
8
9
  var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
9
10
  var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
11
+ var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));
10
12
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
11
13
  var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
12
14
  var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
13
15
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
14
16
  var _ufo = require("../analytics/ufo");
15
- var _l3_vocabulary = _interopRequireDefault(require("./data/l3_vocabulary.json"));
16
- var _vocabulary_10k = _interopRequireDefault(require("./data/vocabulary_10k.json"));
17
- var _word_index_10k = _interopRequireDefault(require("./data/word_index_10k.json"));
18
17
  var _debugMode = require("./debug-mode");
19
18
  var _scoringPipeline = require("./scoring-pipeline");
20
19
  var _slowLaneClient = require("./slow-lane-client");
20
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof3(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t4 in e) "default" !== _t4 && {}.hasOwnProperty.call(e, _t4) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t4)) && (i.get || i.set) ? o(f, _t4, i) : f[_t4] = e[_t4]); return f; })(e, t); }
21
21
  function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
22
22
  function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
23
23
  function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } /**
@@ -35,8 +35,9 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
35
35
  *
36
36
  * Session personalization (L1): words the user types are incrementally boosted
37
37
  * via incrementSessionFreq(), called on word boundaries from the plugin.
38
- */ // import bigramsData from './data/bigrams.json';
39
- // import { rankCandidates, isGrammarAllowed } from './scoring-pipeline';
38
+ */ // The vocabulary, L3 and word-index JSON payloads are dynamically imported in
39
+ // loadDefaultVocabulary / loadVectorsAsync so their (large) contents stay out of
40
+ // the editor's main chunk and only load when autocomplete is initialised.
40
41
  // ─── Constants ───────────────────────────────────────────────────────────────
41
42
 
42
43
  // eslint-disable-next-line require-unicode-regexp
@@ -44,7 +45,7 @@ var PUNCTUATION_BOUNDARY_REGEX = /^[.,;:!?()\[\]{}"'`]+|[.,;:!?()\[\]{}"'`]+$/g;
44
45
  var MIN_PREFIX_LENGTH = 3;
45
46
  var MAX_CANDIDATES = 200;
46
47
  var CONTEXT_WORDS = 10;
47
- var MIN_SCORE_THRESHOLD = 0.2;
48
+ var MIN_SCORE_THRESHOLD = 0.35;
48
49
  var L3_BASELINE_FREQ = 0.001;
49
50
 
50
51
  // ─── Types ───────────────────────────────────────────────────────────────────
@@ -208,7 +209,6 @@ var wordTrie = new WeightedWordTrie();
208
209
  // L3 Trie (General English Fallback)
209
210
  var l3Trie = new WeightedWordTrie();
210
211
 
211
- // --- Initialization Function ---
212
212
  /**
213
213
  * Loads the General English vocabulary.
214
214
  * expects a simple array of strings: ["about", "above", "actually", ...]
@@ -332,14 +332,11 @@ var tokenize = function tokenize(text) {
332
332
  return tokens;
333
333
  };
334
334
  var extractPreviousWord = function extractPreviousWord(text) {
335
- // 1. Split the text by newlines or punctuation (. ? !)
335
+ // Only consider the current sentence/line the user is typing in.
336
336
  // eslint-disable-next-line require-unicode-regexp
337
337
  var sentences = text.split(/[\n.?!]+/);
338
-
339
- // 2. Only look at the current sentence/line the user is typing in
340
338
  var currentSentence = sentences[sentences.length - 1];
341
339
 
342
- // 3. Extract the previous word as normal
343
340
  // eslint-disable-next-line require-unicode-regexp
344
341
  var words = currentSentence.trimEnd().split(/\s+/);
345
342
  return words.length >= 2 ? words[words.length - 2] : '';
@@ -402,7 +399,6 @@ var incrementSessionFreq = exports.incrementSessionFreq = function incrementSess
402
399
  * Pass `undefined` (or omit the argument) to skip priming — useful when the
403
400
  * calling context does not yet have a page value available.
404
401
  */
405
- // NOTE: We ingest full page context here
406
402
  var ingestDocumentPage = exports.ingestDocumentPage = function ingestDocumentPage(pageContent) {
407
403
  if (!pageContent) {
408
404
  return;
@@ -435,7 +431,11 @@ var ingestDocumentPage = exports.ingestDocumentPage = function ingestDocumentPag
435
431
  };
436
432
  var predict = exports.predict = function predict(textBefore) {
437
433
  if (!isInitialized) {
438
- loadDefaultVocabulary();
434
+ // Vocabulary JSON is code-split and loads asynchronously. Kick off the load
435
+ // and skip this keystroke; the plugin also primes it on focus, so the tries
436
+ // are usually ready before the user types.
437
+ void loadDefaultVocabulary().catch(function () {});
438
+ return null;
439
439
  }
440
440
  var t0 = performance.now();
441
441
 
@@ -487,14 +487,11 @@ var predict = exports.predict = function predict(textBefore) {
487
487
  if (currentWord.length < MIN_PREFIX_LENGTH) {
488
488
  return null;
489
489
  }
490
-
491
- // 1. Primary Query: Ask the L2 Domain Trie
492
490
  var candidates = wordTrie.getCandidates(currentWord, MAX_CANDIDATES);
493
491
 
494
- // 2. Fallback Query: Gap-fill with the L3 General English Trie
492
+ // Gap-fill from the L3 general-English trie, requesting a full buffer so
493
+ // enough survive de-duplication against the L2 results.
495
494
  if (candidates.length < MAX_CANDIDATES) {
496
- // Ask L3 for MAX_CANDIDATES to guarantee we have enough buffer
497
- // to survive the deduplication process.
498
495
  var l3Candidates = l3Trie.getCandidates(currentWord, MAX_CANDIDATES);
499
496
  var existingWords = new Set(candidates.map(function (c) {
500
497
  return c.word;
@@ -504,8 +501,7 @@ var predict = exports.predict = function predict(textBefore) {
504
501
  try {
505
502
  for (_iterator0.s(); !(_step0 = _iterator0.n()).done;) {
506
503
  var l3c = _step0.value;
507
- if (candidates.length >= MAX_CANDIDATES) break; // Stop exactly at the limit
508
-
504
+ if (candidates.length >= MAX_CANDIDATES) break;
509
505
  if (!existingWords.has(l3c.word)) {
510
506
  candidates.push(l3c);
511
507
  }
@@ -689,9 +685,55 @@ var predict = exports.predict = function predict(textBefore) {
689
685
 
690
686
  // ─── Data Loading ────────────────────────────────────────────────────────────
691
687
 
688
+ /**
689
+ * Unwrap a dynamically imported JSON module to its parsed value, handling both
690
+ * interop modes AFM's bundler chain emits: a `.default`-wrapped namespace
691
+ * (classic webpack) and a named-exports namespace (webpack 5 / atlaspack JSON
692
+ * modules, where `default` can be a misleading scalar). Named exports are
693
+ * preferred when present. The caller declares the JSON `shape` because a dense
694
+ * array and a sparse numeric-keyed object are emitted identically as named
695
+ * exports. Kept in lock-step with the matching helper in local-slow-lane-client.ts.
696
+ */
697
+ var unwrapJsonModule = function unwrapJsonModule(mod, shape) {
698
+ if (mod == null || (0, _typeof2.default)(mod) !== 'object') {
699
+ return null;
700
+ }
701
+ var namespace = mod;
702
+ var ownKeys = Object.keys(namespace).filter(function (k) {
703
+ return k !== 'default' && k !== '__esModule';
704
+ });
705
+ if (ownKeys.length > 0) {
706
+ if (shape === 'array') {
707
+ var len = ownKeys.length;
708
+ var arr = new Array(len);
709
+ for (var i = 0; i < len; i++) {
710
+ arr[i] = namespace[String(i)];
711
+ }
712
+ return arr;
713
+ }
714
+ var obj = {};
715
+ var _iterator1 = _createForOfIteratorHelper(ownKeys),
716
+ _step1;
717
+ try {
718
+ for (_iterator1.s(); !(_step1 = _iterator1.n()).done;) {
719
+ var k = _step1.value;
720
+ obj[k] = namespace[k];
721
+ }
722
+ } catch (err) {
723
+ _iterator1.e(err);
724
+ } finally {
725
+ _iterator1.f();
726
+ }
727
+ return obj;
728
+ }
729
+ if ('default' in namespace && namespace.default != null) {
730
+ return namespace.default;
731
+ }
732
+ return null;
733
+ };
692
734
  var loadVectorsAsync = exports.loadVectorsAsync = /*#__PURE__*/function () {
693
735
  var _ref6 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(options) {
694
- var url, res, buffer, float32, wordIndex, nWords, dim, _t, _t2;
736
+ var url, _wordIndexOuter$index, res, buffer, float32, wordIndexModule, wordIndexOuter, wordIndex, nWords, dim, _t, _t2;
695
737
  return _regenerator.default.wrap(function (_context) {
696
738
  while (1) switch (_context.prev = _context.next) {
697
739
  case 0:
@@ -751,9 +793,22 @@ var loadVectorsAsync = exports.loadVectorsAsync = /*#__PURE__*/function () {
751
793
  return res.arrayBuffer();
752
794
  case 9:
753
795
  buffer = _context.sent;
754
- float32 = new Float32Array(buffer);
755
- wordIndex = _word_index_10k.default;
796
+ float32 = new Float32Array(buffer); // word_index_10k.json is wrapped as `{ "index": {…} }` so no real entry
797
+ // (e.g. the word "default") can shadow the synthetic ESM `default` export
798
+ // the bundler creates for dynamically-imported JSON.
799
+ _context.next = 10;
800
+ return Promise.resolve().then(function () {
801
+ return _interopRequireWildcard(require( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-word-index-10k" */'./data/word_index_10k.json'));
802
+ });
803
+ case 10:
804
+ wordIndexModule = _context.sent;
805
+ wordIndexOuter = unwrapJsonModule(wordIndexModule, 'object');
806
+ wordIndex = (_wordIndexOuter$index = wordIndexOuter === null || wordIndexOuter === void 0 ? void 0 : wordIndexOuter.index) !== null && _wordIndexOuter$index !== void 0 ? _wordIndexOuter$index : {};
756
807
  nWords = Object.keys(wordIndex).length;
808
+ if (nWords === 0) {
809
+ // eslint-disable-next-line no-console
810
+ console.warn('[text-predictor] word_index_10k.json missing its `index` wrapper — wordIndex is empty, semantic scoring will be a no-op.');
811
+ }
757
812
  dim = float32.length / nWords;
758
813
  vectorStore = {
759
814
  float32: float32,
@@ -773,10 +828,10 @@ var loadVectorsAsync = exports.loadVectorsAsync = /*#__PURE__*/function () {
773
828
  sizeBytes: float32.byteLength
774
829
  });
775
830
  }
776
- _context.next = 11;
831
+ _context.next = 12;
777
832
  break;
778
- case 10:
779
- _context.prev = 10;
833
+ case 11:
834
+ _context.prev = 11;
780
835
  _t2 = _context["catch"](6);
781
836
  vectorsLoadStarted = false;
782
837
  (0, _ufo.failExp)(_ufo.EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
@@ -784,11 +839,11 @@ var loadVectorsAsync = exports.loadVectorsAsync = /*#__PURE__*/function () {
784
839
  });
785
840
  // eslint-disable-next-line no-console
786
841
  console.warn('[text-predictor] Failed to load vectors:', _t2);
787
- case 11:
842
+ case 12:
788
843
  case "end":
789
844
  return _context.stop();
790
845
  }
791
- }, _callee, null, [[3, 5], [6, 10]]);
846
+ }, _callee, null, [[3, 5], [6, 11]]);
792
847
  }));
793
848
  return function loadVectorsAsync(_x) {
794
849
  return _ref6.apply(this, arguments);
@@ -797,40 +852,77 @@ var loadVectorsAsync = exports.loadVectorsAsync = /*#__PURE__*/function () {
797
852
  var initVectors = exports.initVectors = function initVectors(store) {
798
853
  vectorStore = store;
799
854
  };
855
+ var vocabularyLoadPromise;
800
856
  var loadDefaultVocabulary = exports.loadDefaultVocabulary = function loadDefaultVocabulary() {
801
857
  if (isInitialized) {
802
- return;
858
+ return Promise.resolve();
803
859
  }
804
- (0, _ufo.startExp)(_ufo.EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton');
805
- try {
806
- // 1. Load the Atlassian Domain (L2)
807
- var data = _vocabulary_10k.default;
808
- var terms = Object.entries(data.words).map(function (_ref7) {
809
- var _ref8 = (0, _slicedToArray2.default)(_ref7, 2),
810
- word = _ref8[0],
811
- stats = _ref8[1];
812
- return {
813
- word: word,
814
- freq: stats.freq,
815
- docFreq: stats.doc_freq,
816
- authorFreq: stats.author_freq
817
- };
818
- });
819
- initVocabulary({
820
- terms: terms
821
- });
822
-
823
- // 2. Load General English (L3)
824
- var l3Words = _l3_vocabulary.default;
825
- initL3Vocabulary(l3Words);
826
- (0, _ufo.succeedExp)(_ufo.EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
827
- l2WordCount: terms.length,
828
- l3WordCount: l3Words.length
829
- });
830
- } catch (e) {
831
- (0, _ufo.failExp)(_ufo.EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
832
- errorType: 'parse_error'
833
- });
834
- throw e;
860
+ if (vocabularyLoadPromise) {
861
+ return vocabularyLoadPromise;
835
862
  }
863
+ vocabularyLoadPromise = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2() {
864
+ var _yield$Promise$all, _yield$Promise$all2, vocabularyModule, l3VocabularyModule, vocabularyData, l3VocabularyData, terms, _t3;
865
+ return _regenerator.default.wrap(function (_context2) {
866
+ while (1) switch (_context2.prev = _context2.next) {
867
+ case 0:
868
+ (0, _ufo.startExp)(_ufo.EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton');
869
+ _context2.prev = 1;
870
+ _context2.next = 2;
871
+ return Promise.all([Promise.resolve().then(function () {
872
+ return _interopRequireWildcard(require( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-vocabulary-10k" */'./data/vocabulary_10k.json'));
873
+ }), Promise.resolve().then(function () {
874
+ return _interopRequireWildcard(require( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-l3-vocabulary" */'./data/l3_vocabulary.json'));
875
+ })]);
876
+ case 2:
877
+ _yield$Promise$all = _context2.sent;
878
+ _yield$Promise$all2 = (0, _slicedToArray2.default)(_yield$Promise$all, 2);
879
+ vocabularyModule = _yield$Promise$all2[0];
880
+ l3VocabularyModule = _yield$Promise$all2[1];
881
+ vocabularyData = unwrapJsonModule(vocabularyModule, 'object');
882
+ l3VocabularyData = unwrapJsonModule(l3VocabularyModule, 'array');
883
+ if (!((vocabularyData === null || vocabularyData === void 0 ? void 0 : vocabularyData.words) == null || !Array.isArray(l3VocabularyData))) {
884
+ _context2.next = 3;
885
+ break;
886
+ }
887
+ throw new Error('[text-predictor] vocabulary JSON modules could not be unwrapped');
888
+ case 3:
889
+ terms = Object.entries(vocabularyData.words).map(function (_ref8) {
890
+ var _ref9 = (0, _slicedToArray2.default)(_ref8, 2),
891
+ word = _ref9[0],
892
+ stats = _ref9[1];
893
+ return {
894
+ word: word,
895
+ freq: stats.freq,
896
+ docFreq: stats.doc_freq,
897
+ authorFreq: stats.author_freq
898
+ };
899
+ }); // Load L3 before L2: initVocabulary flips `isInitialized = true`, so it
900
+ // must run last — otherwise a throw in initL3Vocabulary would strand
901
+ // `isInitialized` true and the retry path could never reload L3.
902
+ initL3Vocabulary(l3VocabularyData);
903
+ initVocabulary({
904
+ terms: terms
905
+ });
906
+ (0, _ufo.succeedExp)(_ufo.EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
907
+ l2WordCount: terms.length,
908
+ l3WordCount: l3VocabularyData.length
909
+ });
910
+ _context2.next = 5;
911
+ break;
912
+ case 4:
913
+ _context2.prev = 4;
914
+ _t3 = _context2["catch"](1);
915
+ (0, _ufo.failExp)(_ufo.EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
916
+ errorType: 'parse_error'
917
+ });
918
+ // Allow a later call to retry the load rather than caching the failure.
919
+ vocabularyLoadPromise = undefined;
920
+ throw _t3;
921
+ case 5:
922
+ case "end":
923
+ return _context2.stop();
924
+ }
925
+ }, _callee2, null, [[1, 4]]);
926
+ }))();
927
+ return vocabularyLoadPromise;
836
928
  };
@@ -26,8 +26,6 @@ const getTextBeforeCursor = state => {
26
26
  $from
27
27
  } = state.selection;
28
28
  const maxChars = 200;
29
-
30
- // 1. Get the perfectly flattened text of the current block up to the cursor
31
29
  const blockNode = $from.parent;
32
30
  const offsetInBlock = $from.parentOffset;
33
31
  const blockText = blockNode.textContent.slice(0, offsetInBlock);
@@ -36,7 +34,7 @@ const getTextBeforeCursor = state => {
36
34
  }
37
35
  let fullText = blockText;
38
36
 
39
- // 2. Walk backwards through previous blocks
37
+ // Walk backwards through previous blocks until we have enough context.
40
38
  let depth = $from.depth - 1;
41
39
  while (fullText.length < maxChars && depth >= 0) {
42
40
  const parentNode = $from.node(depth);
@@ -245,8 +243,6 @@ export const createAutocompletePlugin = (options, api) => {
245
243
  const {
246
244
  selection
247
245
  } = state;
248
-
249
- // Only predict for cursor selections (not range selections)
250
246
  if (!selection.empty) {
251
247
  return;
252
248
  }
@@ -258,13 +254,9 @@ export const createAutocompletePlugin = (options, api) => {
258
254
  return;
259
255
  }
260
256
  dismissedContext = null;
261
-
262
- // Don't predict if there's not enough context
263
257
  if (textBefore.trim().length < 3) {
264
258
  return;
265
259
  }
266
-
267
- // Tier 1 prediction is synchronous -- no async needed
268
260
  const prediction = predict(textBefore);
269
261
  if (prediction && prediction.length > 0) {
270
262
  const typedLength = getTypedLengthForPrediction(textBefore);
@@ -327,8 +319,7 @@ export const createAutocompletePlugin = (options, api) => {
327
319
  };
328
320
  }
329
321
 
330
- // If the document changed, clear the ghost text
331
- // (new prediction will be scheduled from view.update)
322
+ // A new prediction is scheduled from view.update.
332
323
  if (tr.docChanged) {
333
324
  return {
334
325
  ...pluginState,
@@ -337,8 +328,6 @@ export const createAutocompletePlugin = (options, api) => {
337
328
  decorationSet: DecorationSet.empty
338
329
  };
339
330
  }
340
-
341
- // If selection changed without doc change, clear ghost text
342
331
  if (tr.selectionSet && pluginState.ghostText) {
343
332
  return {
344
333
  ...pluginState,
@@ -400,13 +389,11 @@ export const createAutocompletePlugin = (options, api) => {
400
389
  return false;
401
390
  },
402
391
  focus: () => {
403
- try {
404
- loadDefaultVocabulary();
405
- } catch (error) {
392
+ loadDefaultVocabulary().catch(error => {
406
393
  logException(error, {
407
394
  location: 'editor-plugin-autocomplete/loadDefaultVocabulary'
408
395
  });
409
- }
396
+ });
410
397
  loadVectorsAsync({
411
398
  getBinaryUrl: options === null || options === void 0 ? void 0 : options.getVectorsBinaryUrl
412
399
  }).catch(error => {
@@ -447,12 +434,9 @@ export const createAutocompletePlugin = (options, api) => {
447
434
  if (justAccepted) {
448
435
  justAccepted = false;
449
436
 
450
- // ✨ THE FIX: Memorize the text state right after acceptance.
451
- // Any follow-up transactions will hit the 'dismissedContext'
452
- // block and abort until the user actually types a new character!
437
+ // Snapshot the post-acceptance text so follow-up transactions hit
438
+ // the dismissedContext guard and abort until the user types again.
453
439
  dismissedContext = getTextBeforeCursor(view.state);
454
-
455
- // Also clear any pending debounce timers from before the acceptance
456
440
  if (debounceTimer) {
457
441
  clearTimeout(debounceTimer);
458
442
  }
@@ -135,15 +135,16 @@ function applyGrammarFilter(candidates, previousWord) {
135
135
  dropped.push(entry.candidate.word);
136
136
  }
137
137
  }
138
- const finalFiltered = filtered.length > 0 ? filtered : candidates;
138
+
139
+ // Grammar is authoritative.
139
140
  return {
140
- filtered: finalFiltered,
141
+ filtered: filtered,
141
142
  grammarMeta: {
142
143
  prevWord: lowerPrev,
143
144
  prevTags,
144
145
  before: candidates.length,
145
- after: finalFiltered.length,
146
- dropped: filtered.length > 0 ? dropped : []
146
+ after: filtered.length,
147
+ dropped: dropped
147
148
  }
148
149
  };
149
150
  }