@atlaskit/editor-plugin-autocomplete 8.0.0 → 9.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 +108 -0
- package/dist/cjs/analytics/ufo.js +2 -1
- package/dist/cjs/pm-plugins/artifact-loader.js +135 -0
- package/dist/cjs/pm-plugins/artifacts-manifest.js +3 -0
- package/dist/cjs/pm-plugins/autocomplete-plugin.js +498 -160
- package/dist/cjs/pm-plugins/canonical-lm-scoring.js +371 -0
- package/dist/cjs/pm-plugins/debug-mode.js +105 -7
- package/dist/cjs/pm-plugins/ghost-text-decoration.js +12 -3
- package/dist/cjs/pm-plugins/local-slow-lane-client.js +1202 -453
- package/dist/cjs/pm-plugins/scoring-pipeline.js +164 -19
- package/dist/cjs/pm-plugins/slow-lane-client.js +100 -37
- package/dist/cjs/pm-plugins/text-predictor.js +2099 -309
- package/dist/es2019/analytics/ufo.js +2 -0
- package/dist/es2019/pm-plugins/artifact-loader.js +70 -0
- package/dist/es2019/pm-plugins/artifacts-manifest.js +3 -0
- package/dist/es2019/pm-plugins/autocomplete-plugin.js +489 -152
- package/dist/es2019/pm-plugins/canonical-lm-scoring.js +270 -0
- package/dist/es2019/pm-plugins/debug-mode.js +102 -7
- package/dist/es2019/pm-plugins/ghost-text-decoration.js +12 -3
- package/dist/es2019/pm-plugins/local-slow-lane-client.js +940 -326
- package/dist/es2019/pm-plugins/scoring-pipeline.js +137 -13
- package/dist/es2019/pm-plugins/slow-lane-client.js +94 -37
- package/dist/es2019/pm-plugins/text-predictor.js +1692 -200
- package/dist/esm/analytics/ufo.js +2 -1
- package/dist/esm/pm-plugins/artifact-loader.js +128 -0
- package/dist/esm/pm-plugins/artifacts-manifest.js +3 -0
- package/dist/esm/pm-plugins/autocomplete-plugin.js +501 -163
- package/dist/esm/pm-plugins/canonical-lm-scoring.js +364 -0
- package/dist/esm/pm-plugins/debug-mode.js +104 -6
- package/dist/esm/pm-plugins/ghost-text-decoration.js +12 -3
- package/dist/esm/pm-plugins/local-slow-lane-client.js +1200 -451
- package/dist/esm/pm-plugins/scoring-pipeline.js +163 -18
- package/dist/esm/pm-plugins/slow-lane-client.js +100 -37
- package/dist/esm/pm-plugins/text-predictor.js +2101 -313
- package/dist/types/analytics/ufo.d.ts +1 -0
- package/dist/types/pm-plugins/artifact-loader.d.ts +44 -0
- package/dist/types/pm-plugins/artifacts-manifest.d.ts +3 -0
- package/dist/types/pm-plugins/autocomplete-plugin.d.ts +39 -0
- package/dist/types/pm-plugins/canonical-lm-scoring.d.ts +158 -0
- package/dist/types/pm-plugins/debug-mode.d.ts +55 -1
- package/dist/types/pm-plugins/ghost-text-decoration.d.ts +5 -2
- package/dist/types/pm-plugins/local-slow-lane-client.d.ts +87 -54
- package/dist/types/pm-plugins/scoring-pipeline.d.ts +33 -1
- package/dist/types/pm-plugins/slow-lane-client.d.ts +51 -3
- package/dist/types/pm-plugins/text-predictor.d.ts +156 -3
- package/package.json +2 -2
- package/src/analytics/ufo.ts +5 -0
- package/src/pm-plugins/artifact-loader.ts +92 -0
- package/src/pm-plugins/artifacts-manifest.ts +3 -0
- package/src/pm-plugins/autocomplete-plugin.ts +584 -154
- package/src/pm-plugins/canonical-lm-scoring.ts +412 -0
- package/src/pm-plugins/debug-mode.ts +134 -7
- package/src/pm-plugins/ghost-text-decoration.ts +13 -4
- package/src/pm-plugins/local-slow-lane-client.ts +1302 -386
- package/src/pm-plugins/scoring-pipeline.ts +167 -17
- package/src/pm-plugins/slow-lane-client.ts +147 -62
- package/src/pm-plugins/text-predictor.ts +2119 -254
- package/CANONICAL_FIX__DO_NOT_USE_ME_A/package.json +0 -8
- package/CANONICAL_FIX__DO_NOT_USE_ME_B/package.json +0 -8
- package/CANONICAL_FIX__DO_NOT_USE_ME_C/package.json +0 -8
- package/dist/cjs/entry-points/src-pm-plugins-autocomplete-plugin.js +0 -18
- package/dist/cjs/entry-points/src-pm-plugins-slow-lane-client.js +0 -36
- package/dist/cjs/entry-points/src-pm-plugins-text-predictor.js +0 -66
- package/dist/es2019/entry-points/src-pm-plugins-autocomplete-plugin.js +0 -2
- package/dist/es2019/entry-points/src-pm-plugins-slow-lane-client.js +0 -2
- package/dist/es2019/entry-points/src-pm-plugins-text-predictor.js +0 -2
- package/dist/esm/entry-points/src-pm-plugins-autocomplete-plugin.js +0 -2
- package/dist/esm/entry-points/src-pm-plugins-slow-lane-client.js +0 -2
- package/dist/esm/entry-points/src-pm-plugins-text-predictor.js +0 -2
- package/dist/types/entry-points/src-pm-plugins-autocomplete-plugin.d.ts +0 -2
- package/dist/types/entry-points/src-pm-plugins-slow-lane-client.d.ts +0 -2
- package/dist/types/entry-points/src-pm-plugins-text-predictor.d.ts +0 -2
- package/scripts/gen_first_token_to_words.py +0 -170
- package/src/entry-points/src-pm-plugins-autocomplete-plugin.ts +0 -7
- package/src/entry-points/src-pm-plugins-slow-lane-client.ts +0 -13
- package/src/entry-points/src-pm-plugins-text-predictor.ts +0 -14
- package/src/pm-plugins/autocomplete-plugin/package.json +0 -8
- package/src/pm-plugins/slow-lane-client/package.json +0 -8
- package/src/pm-plugins/text-predictor/package.json +0 -8
|
@@ -4,14 +4,15 @@ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
|
4
4
|
import { keydownHandler } from '@atlaskit/editor-prosemirror/keymap';
|
|
5
5
|
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
|
|
6
6
|
import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
|
|
7
|
-
import { isAutocompleteDebugEnabled } from './debug-mode';
|
|
7
|
+
import { CTC_STYLES, ctcTag, isAutocompleteDebugEnabled, isAutocompleteDebugVerbose } from './debug-mode';
|
|
8
8
|
import { createGhostTextDecorationSet } from './ghost-text-decoration';
|
|
9
9
|
import { createLocalSlowLaneClient } from './local-slow-lane-client';
|
|
10
10
|
import { loadGrammarDataAsync } from './scoring-pipeline';
|
|
11
|
-
import { createSlowLaneClient,
|
|
12
|
-
import { predict, loadDefaultVocabulary, loadVectorsAsync, incrementSessionFreq, ingestDocumentPage } from './text-predictor';
|
|
11
|
+
import { clearDefaultSlowLaneClient, createSlowLaneClient, getDefaultSlowLaneClientStatus, isWordBoundary, setDefaultSlowLaneClient } from './slow-lane-client';
|
|
12
|
+
import { predict, loadDefaultVocabulary, loadVectorsAsync, incrementSessionFreq, ingestDocumentPage, getLastPredictionDebug, noteSuggestionAccepted } from './text-predictor';
|
|
13
13
|
export const autocompletePluginKey = new PluginKey('autocomplete');
|
|
14
|
-
const
|
|
14
|
+
const PREDICTION_COALESCE_MS = 0;
|
|
15
|
+
const GHOST_DECISION_BUDGET_MS = 100;
|
|
15
16
|
const NETWORK_SLOW_LANE_DEBOUNCE_MS = 300;
|
|
16
17
|
const LOCAL_SLOW_LANE_DEBOUNCE_MS = 100;
|
|
17
18
|
const CONTEXT_REFRESH_THROTTLE_MS = 1000;
|
|
@@ -26,18 +27,82 @@ const MAX_INGESTED_CONTEXT_TEXTS = 50;
|
|
|
26
27
|
// on every word boundary for the plugin's lifetime.
|
|
27
28
|
const MAX_CONTEXT_REFRESH_ATTEMPTS = 5;
|
|
28
29
|
const INLINE_LEAF_TEXT = ' ';
|
|
30
|
+
let slowLaneClientInstanceSequence = 0;
|
|
29
31
|
|
|
30
|
-
// eslint-disable-next-line require-unicode-regexp
|
|
31
|
-
const TRAILING_NON_BOUNDARY_TOKEN_REGEX = /([^\s.,;:!?]*)$/;
|
|
32
32
|
// eslint-disable-next-line require-unicode-regexp
|
|
33
33
|
const WORD_BOUNDARY_CHARS_REGEX = /[\s.,;:!?]/;
|
|
34
|
+
const TRAILING_SURFACE_PREFIX_REGEX = /[\p{L}\p{N}_'-]+$/u;
|
|
34
35
|
const hasDestroy = client => 'destroy' in client && typeof client.destroy === 'function';
|
|
35
36
|
const createInitialState = () => ({
|
|
36
37
|
ghostText: '',
|
|
37
38
|
ghostPosition: -1,
|
|
38
|
-
decorationSet: DecorationSet.empty
|
|
39
|
+
decorationSet: DecorationSet.empty,
|
|
40
|
+
suggestion: null
|
|
41
|
+
});
|
|
42
|
+
const clearGhostState = pluginState => ({
|
|
43
|
+
...pluginState,
|
|
44
|
+
ghostText: '',
|
|
45
|
+
ghostPosition: -1,
|
|
46
|
+
decorationSet: DecorationSet.empty,
|
|
47
|
+
suggestion: null
|
|
39
48
|
});
|
|
40
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Keep a displayed ghost alive when the user types the character it already
|
|
52
|
+
* predicted.
|
|
53
|
+
*
|
|
54
|
+
* Clearing on every document change throws away a suggestion the keystroke just
|
|
55
|
+
* confirmed, and the replacement decision has to re-earn it from an empty state
|
|
56
|
+
* inside the budget — which it often misses, so the ghost blinks out mid-word.
|
|
57
|
+
* It is worse for multi-word surfaces: typing the space in `machine learning`
|
|
58
|
+
* empties the current word, and the display gate refuses to show anything again
|
|
59
|
+
* until three characters of `learning` exist.
|
|
60
|
+
*
|
|
61
|
+
* A confirming keystroke is not new information about the prediction, so the
|
|
62
|
+
* ghost simply advances by one character and the still-running decision is free
|
|
63
|
+
* to replace it with something better. Anything else — a different character,
|
|
64
|
+
* a paste, a deletion, an edit elsewhere — clears as before.
|
|
65
|
+
*/
|
|
66
|
+
const advanceGhostThroughTypedCharacter = (tr, pluginState) => {
|
|
67
|
+
const {
|
|
68
|
+
ghostPosition,
|
|
69
|
+
ghostText,
|
|
70
|
+
suggestion
|
|
71
|
+
} = pluginState;
|
|
72
|
+
if (ghostText.length < 2 || ghostPosition < 0 || !suggestion) {
|
|
73
|
+
// A one-character tail leaves nothing to show after advancing.
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Exactly one character longer, with the cursor sitting just past the ghost
|
|
78
|
+
// anchor: together these identify a single insertion made at the anchor
|
|
79
|
+
// rather than a paste or an edit elsewhere in the document.
|
|
80
|
+
if (tr.doc.content.size - tr.before.content.size !== 1) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
if (!tr.selection.empty || tr.selection.from !== ghostPosition + 1) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (tr.doc.textBetween(ghostPosition, ghostPosition + 1) !== ghostText[0]) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const remaining = ghostText.slice(1);
|
|
90
|
+
const nextPosition = ghostPosition + 1;
|
|
91
|
+
return {
|
|
92
|
+
...pluginState,
|
|
93
|
+
ghostText: remaining,
|
|
94
|
+
ghostPosition: nextPosition,
|
|
95
|
+
decorationSet: createGhostTextDecorationSet(tr.doc, nextPosition, remaining),
|
|
96
|
+
// `surface` stays the full candidate so cooldown and analytics still
|
|
97
|
+
// describe the suggestion that was originally committed.
|
|
98
|
+
suggestion: {
|
|
99
|
+
...suggestion,
|
|
100
|
+
ghostText: remaining,
|
|
101
|
+
position: nextPosition
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
41
106
|
/**
|
|
42
107
|
* Extract text content before the cursor from the current document.
|
|
43
108
|
* Returns the last ~200 characters for context.
|
|
@@ -70,6 +135,10 @@ const getTextBeforeCursor = state => {
|
|
|
70
135
|
}
|
|
71
136
|
return fullText.slice(-maxChars);
|
|
72
137
|
};
|
|
138
|
+
const getTrailingSurfacePrefixLength = text => {
|
|
139
|
+
var _text$trimEnd$match$, _text$trimEnd$match;
|
|
140
|
+
return (_text$trimEnd$match$ = (_text$trimEnd$match = text.trimEnd().match(TRAILING_SURFACE_PREFIX_REGEX)) === null || _text$trimEnd$match === void 0 ? void 0 : _text$trimEnd$match[0].length) !== null && _text$trimEnd$match$ !== void 0 ? _text$trimEnd$match$ : 0;
|
|
141
|
+
};
|
|
73
142
|
|
|
74
143
|
/**
|
|
75
144
|
* Set the autocomplete state via a transaction metadata.
|
|
@@ -82,23 +151,41 @@ const setAutocompleteMeta = (tr, meta) => {
|
|
|
82
151
|
* Apply a ghost text suggestion to the editor state.
|
|
83
152
|
*/
|
|
84
153
|
let lastShownGhostText = '';
|
|
85
|
-
const showGhostText = (view,
|
|
154
|
+
const showGhostText = (view, prediction, position, revision, decisionStartedAt) => {
|
|
86
155
|
try {
|
|
87
156
|
const {
|
|
88
157
|
state,
|
|
89
158
|
dispatch
|
|
90
159
|
} = view;
|
|
91
|
-
const
|
|
160
|
+
const suggestion = {
|
|
161
|
+
decisionLatencyMs: performance.now() - decisionStartedAt,
|
|
162
|
+
evidenceTier: prediction.evidenceTier,
|
|
163
|
+
ghostText: prediction.text,
|
|
164
|
+
meanTokenLogProbability: prediction.meanTokenLogProbability,
|
|
165
|
+
poolHeldExtension: prediction.poolHeldExtension,
|
|
166
|
+
position,
|
|
167
|
+
posterior: prediction.posterior,
|
|
168
|
+
rankScore: prediction.rankScore,
|
|
169
|
+
revision,
|
|
170
|
+
shortlistSize: prediction.shortlistSize,
|
|
171
|
+
surface: prediction.surface,
|
|
172
|
+
termType: prediction.termType,
|
|
173
|
+
winnerMargin: prediction.winnerMargin
|
|
174
|
+
};
|
|
175
|
+
const decorationSet = createGhostTextDecorationSet(state.doc, position, prediction.text);
|
|
92
176
|
const tr = setAutocompleteMeta(state.tr, {
|
|
93
|
-
ghostText: text,
|
|
177
|
+
ghostText: prediction.text,
|
|
94
178
|
ghostPosition: position,
|
|
95
|
-
decorationSet
|
|
179
|
+
decorationSet,
|
|
180
|
+
suggestion
|
|
96
181
|
});
|
|
97
182
|
dispatch(tr);
|
|
183
|
+
return suggestion;
|
|
98
184
|
} catch (error) {
|
|
99
185
|
logException(error, {
|
|
100
186
|
location: 'editor-plugin-autocomplete/showGhostText'
|
|
101
187
|
});
|
|
188
|
+
return null;
|
|
102
189
|
}
|
|
103
190
|
};
|
|
104
191
|
|
|
@@ -115,7 +202,8 @@ const clearGhostText = (state, dispatch) => {
|
|
|
115
202
|
const tr = setAutocompleteMeta(state.tr, {
|
|
116
203
|
ghostText: '',
|
|
117
204
|
ghostPosition: -1,
|
|
118
|
-
decorationSet: DecorationSet.empty
|
|
205
|
+
decorationSet: DecorationSet.empty,
|
|
206
|
+
suggestion: null
|
|
119
207
|
});
|
|
120
208
|
dispatch(tr);
|
|
121
209
|
} catch (error) {
|
|
@@ -133,40 +221,43 @@ const clearGhostText = (state, dispatch) => {
|
|
|
133
221
|
*/
|
|
134
222
|
const acceptGhostText = (state, dispatch) => {
|
|
135
223
|
const pluginState = autocompletePluginKey.getState(state);
|
|
136
|
-
if (!pluginState || !pluginState.ghostText) {
|
|
137
|
-
return
|
|
224
|
+
if (!(pluginState !== null && pluginState !== void 0 && pluginState.suggestion) || !pluginState.ghostText) {
|
|
225
|
+
return null;
|
|
138
226
|
}
|
|
139
227
|
if (dispatch) {
|
|
140
228
|
try {
|
|
141
229
|
const {
|
|
142
230
|
ghostText,
|
|
143
|
-
ghostPosition
|
|
231
|
+
ghostPosition,
|
|
232
|
+
suggestion
|
|
144
233
|
} = pluginState;
|
|
145
234
|
let tr = state.tr.insertText(ghostText, ghostPosition);
|
|
146
235
|
tr = setAutocompleteMeta(tr, {
|
|
147
236
|
ghostText: '',
|
|
148
237
|
ghostPosition: -1,
|
|
149
|
-
decorationSet: DecorationSet.empty
|
|
238
|
+
decorationSet: DecorationSet.empty,
|
|
239
|
+
suggestion: null
|
|
150
240
|
});
|
|
151
241
|
dispatch(tr);
|
|
242
|
+
return suggestion;
|
|
152
243
|
} catch (error) {
|
|
153
244
|
logException(error, {
|
|
154
245
|
location: 'editor-plugin-autocomplete/acceptGhostText'
|
|
155
246
|
});
|
|
156
|
-
return
|
|
247
|
+
return null;
|
|
157
248
|
}
|
|
158
249
|
}
|
|
159
|
-
return
|
|
250
|
+
return pluginState.suggestion;
|
|
160
251
|
};
|
|
161
252
|
const acceptGhostTextWithAnalytics = (state, dispatch, onAccepted) => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
onAccepted(
|
|
253
|
+
const acceptedSuggestion = acceptGhostText(state, dispatch);
|
|
254
|
+
if (acceptedSuggestion) {
|
|
255
|
+
// Start the post-accept cooldown so the just-accepted unit isn't immediately
|
|
256
|
+
// re-offered (QI-2 echo, e.g. `end to end` → `end to end to end`).
|
|
257
|
+
noteSuggestionAccepted(acceptedSuggestion.surface);
|
|
258
|
+
onAccepted(acceptedSuggestion);
|
|
168
259
|
}
|
|
169
|
-
return
|
|
260
|
+
return acceptedSuggestion !== null;
|
|
170
261
|
};
|
|
171
262
|
|
|
172
263
|
/**
|
|
@@ -228,14 +319,6 @@ const getLeadingTextCharacter = text => {
|
|
|
228
319
|
}
|
|
229
320
|
return String.fromCodePoint(firstCodePoint);
|
|
230
321
|
};
|
|
231
|
-
const getTypedLengthForPrediction = textBefore => {
|
|
232
|
-
var _textBefore$match$, _textBefore$match;
|
|
233
|
-
if (isWordBoundary(textBefore)) {
|
|
234
|
-
return 0;
|
|
235
|
-
}
|
|
236
|
-
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$ : '';
|
|
237
|
-
return trailingToken.length;
|
|
238
|
-
};
|
|
239
322
|
const isEnglishLocale = locale => {
|
|
240
323
|
if (!locale) {
|
|
241
324
|
return false;
|
|
@@ -243,11 +326,12 @@ const isEnglishLocale = locale => {
|
|
|
243
326
|
return locale.toLowerCase().startsWith('en');
|
|
244
327
|
};
|
|
245
328
|
export const createAutocompletePlugin = (options, api) => {
|
|
246
|
-
var _options$surface, _options$locale;
|
|
329
|
+
var _options$surface, _options$locale, _registeredSlowLaneSt;
|
|
247
330
|
const surface = (_options$surface = options === null || options === void 0 ? void 0 : options.surface) !== null && _options$surface !== void 0 ? _options$surface : 'editor';
|
|
248
331
|
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;
|
|
249
332
|
const isAutocompleteEnabled = isEnglishLocale(locale);
|
|
250
333
|
let debounceTimer = null;
|
|
334
|
+
let decisionDeadlineTimer = null;
|
|
251
335
|
let hasIngestedPage = false;
|
|
252
336
|
let resolvedContext;
|
|
253
337
|
/**
|
|
@@ -257,6 +341,10 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
257
341
|
*/
|
|
258
342
|
let currentView = null;
|
|
259
343
|
let unsubscribeFromContextUpdates;
|
|
344
|
+
// "Prime, don't ask-and-read": semantic, Tier-A, and Tier-B results land
|
|
345
|
+
// asynchronously, often with no following keystroke. Re-run prediction when
|
|
346
|
+
// they arrive so the caches are consumed immediately.
|
|
347
|
+
let reschedulePredictionOnSlowLaneResult = null;
|
|
260
348
|
/**
|
|
261
349
|
* Set after accepting a suggestion so the next doc-change update
|
|
262
350
|
* skips scheduling a new prediction for the just-inserted text.
|
|
@@ -270,8 +358,8 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
270
358
|
* re-showing the same suggestion. Resets to null as soon as the text changes.
|
|
271
359
|
*/
|
|
272
360
|
let dismissedContext = null;
|
|
273
|
-
let
|
|
274
|
-
let
|
|
361
|
+
let decisionRevision = 0;
|
|
362
|
+
let activeDecision;
|
|
275
363
|
|
|
276
364
|
/**
|
|
277
365
|
* cold → no slow-lane vector received yet; frequency-only trie scoring
|
|
@@ -338,10 +426,10 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
338
426
|
}
|
|
339
427
|
});
|
|
340
428
|
};
|
|
341
|
-
const fireSuggestionInsertedAnalytics =
|
|
429
|
+
const fireSuggestionInsertedAnalytics = suggestion => {
|
|
342
430
|
var _api$analytics4;
|
|
343
|
-
const typedLength =
|
|
344
|
-
const suggestionLength =
|
|
431
|
+
const typedLength = Math.max(0, suggestion.surface.length - suggestion.ghostText.length);
|
|
432
|
+
const suggestionLength = suggestion.surface.length;
|
|
345
433
|
const kssDelta = suggestionLength - typedLength;
|
|
346
434
|
api === null || api === void 0 ? void 0 : (_api$analytics4 = api.analytics) === null || _api$analytics4 === void 0 ? void 0 : _api$analytics4.actions.fireAnalyticsEvent({
|
|
347
435
|
action: ACTION.SUGGESTION_INSERTED,
|
|
@@ -360,13 +448,61 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
360
448
|
debounceMs: LOCAL_SLOW_LANE_DEBOUNCE_MS,
|
|
361
449
|
onLoadSuccess: fireLocalModelLoadedAnalytics,
|
|
362
450
|
onLoadError: fireLocalModelLoadFailedAnalytics,
|
|
451
|
+
// Re-run prediction for each independently scheduled model result.
|
|
452
|
+
onUpdate: () => {
|
|
453
|
+
var _reschedulePrediction;
|
|
454
|
+
return (_reschedulePrediction = reschedulePredictionOnSlowLaneResult) === null || _reschedulePrediction === void 0 ? void 0 : _reschedulePrediction({
|
|
455
|
+
kind: 'semantic'
|
|
456
|
+
});
|
|
457
|
+
},
|
|
458
|
+
onBoundaryLmUpdate: ({
|
|
459
|
+
contextKey,
|
|
460
|
+
familyKey,
|
|
461
|
+
latencyMs
|
|
462
|
+
}) => {
|
|
463
|
+
var _reschedulePrediction2;
|
|
464
|
+
return (_reschedulePrediction2 = reschedulePredictionOnSlowLaneResult) === null || _reschedulePrediction2 === void 0 ? void 0 : _reschedulePrediction2({
|
|
465
|
+
contextKey,
|
|
466
|
+
familyKey,
|
|
467
|
+
kind: 'boundary',
|
|
468
|
+
latencyMs
|
|
469
|
+
});
|
|
470
|
+
},
|
|
471
|
+
onSurfaceScoreUpdate: ({
|
|
472
|
+
contextKey,
|
|
473
|
+
familyKey,
|
|
474
|
+
latencyMs
|
|
475
|
+
}) => {
|
|
476
|
+
var _reschedulePrediction3;
|
|
477
|
+
return (_reschedulePrediction3 = reschedulePredictionOnSlowLaneResult) === null || _reschedulePrediction3 === void 0 ? void 0 : _reschedulePrediction3({
|
|
478
|
+
contextKey,
|
|
479
|
+
familyKey,
|
|
480
|
+
kind: 'surface',
|
|
481
|
+
latencyMs
|
|
482
|
+
});
|
|
483
|
+
},
|
|
363
484
|
surface
|
|
364
485
|
}) : createSlowLaneClient({
|
|
365
486
|
baseUrl: '',
|
|
366
487
|
debounceMs: NETWORK_SLOW_LANE_DEBOUNCE_MS,
|
|
488
|
+
// Re-run prediction when the BE semantic vector / LM logits land so
|
|
489
|
+
// warm-mode unigram ranking applies without waiting for a keystroke.
|
|
490
|
+
onUpdate: () => {
|
|
491
|
+
var _reschedulePrediction4;
|
|
492
|
+
return (_reschedulePrediction4 = reschedulePredictionOnSlowLaneResult) === null || _reschedulePrediction4 === void 0 ? void 0 : _reschedulePrediction4({
|
|
493
|
+
kind: 'semantic'
|
|
494
|
+
});
|
|
495
|
+
},
|
|
367
496
|
surface
|
|
368
497
|
});
|
|
369
|
-
|
|
498
|
+
const slowLaneClientKind = options !== null && options !== void 0 && options.useLocalModel ? 'localLlm' : 'server';
|
|
499
|
+
const slowLaneClientId = `${surface}:${slowLaneClientKind}:${++slowLaneClientInstanceSequence}`;
|
|
500
|
+
setDefaultSlowLaneClient(slowLaneClient, {
|
|
501
|
+
clientId: slowLaneClientId,
|
|
502
|
+
kind: slowLaneClientKind
|
|
503
|
+
});
|
|
504
|
+
const registeredSlowLaneStatus = getDefaultSlowLaneClientStatus();
|
|
505
|
+
ctcTag('init', `slow lane registered · id=${slowLaneClientId} · selected=${slowLaneClientKind} · canonical=${registeredSlowLaneStatus.canonicalScoringSupported ? 'yes' : 'no'} · ready=${(_registeredSlowLaneSt = registeredSlowLaneStatus.localModelReady) !== null && _registeredSlowLaneSt !== void 0 ? _registeredSlowLaneSt : 'n/a'}`, slowLaneClientKind === 'localLlm' ? CTC_STYLES.good : CTC_STYLES.warn);
|
|
370
506
|
let contextRequestInFlight = false;
|
|
371
507
|
let lastContextRefreshAt = 0;
|
|
372
508
|
// Bounds the word-boundary retry loop so it terminates even when the editor is
|
|
@@ -375,7 +511,63 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
375
511
|
// Set when the plugin is torn down so in-flight getContext() resolutions don't
|
|
376
512
|
// mutate the global text-predictor state after destruction.
|
|
377
513
|
let destroyed = false;
|
|
378
|
-
|
|
514
|
+
// L1 ingestion cannot run before the vocabulary has loaded: `incrementSessionFreq`
|
|
515
|
+
// only touches trie nodes that already exist, so an empty trie silently rejects
|
|
516
|
+
// every word. Text that arrives first — which a page usually does, since the
|
|
517
|
+
// chat resolves it on focus in the same tick as the load starts — is held here
|
|
518
|
+
// and applied once the load has settled.
|
|
519
|
+
const pendingSessionContextTexts = new Set();
|
|
520
|
+
const sessionIngestedContextTexts = new Set();
|
|
521
|
+
let isVocabularyReady = false;
|
|
522
|
+
let vocabularyLoadPromise;
|
|
523
|
+
const addBoundedContextText = (texts, text) => {
|
|
524
|
+
texts.add(text);
|
|
525
|
+
// Evict oldest entries (Set preserves insertion order) to bound memory.
|
|
526
|
+
while (texts.size > MAX_INGESTED_CONTEXT_TEXTS) {
|
|
527
|
+
const oldest = texts.values().next().value;
|
|
528
|
+
if (oldest === undefined) {
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
texts.delete(oldest);
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
const flushPendingSessionContext = () => {
|
|
535
|
+
if (!isVocabularyReady || destroyed) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
for (const text of pendingSessionContextTexts) {
|
|
539
|
+
if (!sessionIngestedContextTexts.has(text)) {
|
|
540
|
+
ingestDocumentPage(text);
|
|
541
|
+
addBoundedContextText(sessionIngestedContextTexts, text);
|
|
542
|
+
}
|
|
543
|
+
pendingSessionContextTexts.delete(text);
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
const ensureVocabularyReady = () => {
|
|
547
|
+
var _options$useLocalMode;
|
|
548
|
+
if (isVocabularyReady) {
|
|
549
|
+
flushPendingSessionContext();
|
|
550
|
+
return Promise.resolve();
|
|
551
|
+
}
|
|
552
|
+
vocabularyLoadPromise ??= loadDefaultVocabulary({
|
|
553
|
+
isLocalLLM: (_options$useLocalMode = options === null || options === void 0 ? void 0 : options.useLocalModel) !== null && _options$useLocalMode !== void 0 ? _options$useLocalMode : false,
|
|
554
|
+
surface
|
|
555
|
+
}).then(() => {
|
|
556
|
+
if (destroyed) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
isVocabularyReady = true;
|
|
560
|
+
flushPendingSessionContext();
|
|
561
|
+
}).catch(error => {
|
|
562
|
+
// Do not consume the pending text on failure. A later focus retries the
|
|
563
|
+
// load and can still apply the original context exactly once.
|
|
564
|
+
vocabularyLoadPromise = undefined;
|
|
565
|
+
logException(error, {
|
|
566
|
+
location: 'editor-plugin-autocomplete/loadDefaultVocabulary'
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
return vocabularyLoadPromise;
|
|
570
|
+
};
|
|
379
571
|
const logContextResolved = (source, context) => {
|
|
380
572
|
var _context$parentCommen, _context$siblingComme4, _context$siblingComme5;
|
|
381
573
|
if (!isAutocompleteDebugEnabled()) {
|
|
@@ -383,7 +575,7 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
383
575
|
}
|
|
384
576
|
|
|
385
577
|
// eslint-disable-next-line no-console
|
|
386
|
-
console.log('%c[
|
|
578
|
+
console.log('%c[CTC:signal]%c getContext resolved', CTC_STYLES.brand, CTC_STYLES.body, {
|
|
387
579
|
source,
|
|
388
580
|
hasParentComment: !!(context !== null && context !== void 0 && context.parentCommentContent),
|
|
389
581
|
parentCommentPreview: context === null || context === void 0 ? void 0 : (_context$parentCommen = context.parentCommentContent) === null || _context$parentCommen === void 0 ? void 0 : _context$parentCommen.slice(0, 80),
|
|
@@ -403,19 +595,10 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
403
595
|
...definedContext
|
|
404
596
|
};
|
|
405
597
|
const ingestContextText = text => {
|
|
406
|
-
if (!text ||
|
|
598
|
+
if (!text || sessionIngestedContextTexts.has(text)) {
|
|
407
599
|
return;
|
|
408
600
|
}
|
|
409
|
-
|
|
410
|
-
// Evict oldest entries (Set preserves insertion order) to bound memory.
|
|
411
|
-
while (ingestedContextTexts.size > MAX_INGESTED_CONTEXT_TEXTS) {
|
|
412
|
-
const oldest = ingestedContextTexts.values().next().value;
|
|
413
|
-
if (oldest === undefined) {
|
|
414
|
-
break;
|
|
415
|
-
}
|
|
416
|
-
ingestedContextTexts.delete(oldest);
|
|
417
|
-
}
|
|
418
|
-
ingestDocumentPage(text);
|
|
601
|
+
addBoundedContextText(pendingSessionContextTexts, text);
|
|
419
602
|
};
|
|
420
603
|
ingestContextText(context.fullPageContent);
|
|
421
604
|
ingestContextText(context.parentCommentContent);
|
|
@@ -423,6 +606,7 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
423
606
|
var _context$siblingComme6;
|
|
424
607
|
ingestContextText(siblingCommentContent);
|
|
425
608
|
}
|
|
609
|
+
flushPendingSessionContext();
|
|
426
610
|
|
|
427
611
|
// Context arrived after word boundaries may already have fired. Re-send
|
|
428
612
|
// slow-lane context immediately so the next inference includes the thread.
|
|
@@ -470,77 +654,239 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
470
654
|
});
|
|
471
655
|
return true;
|
|
472
656
|
};
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Schedule a prediction after a short debounce.
|
|
476
|
-
* Tier 1 predictions are synchronous (<0.1ms) but we still debounce
|
|
477
|
-
* to avoid unnecessary work on rapid keystrokes.
|
|
478
|
-
*/
|
|
479
|
-
const schedulePrediction = view => {
|
|
657
|
+
const clearDecisionTimers = () => {
|
|
480
658
|
if (debounceTimer) {
|
|
481
659
|
clearTimeout(debounceTimer);
|
|
660
|
+
debounceTimer = null;
|
|
482
661
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
662
|
+
if (decisionDeadlineTimer) {
|
|
663
|
+
clearTimeout(decisionDeadlineTimer);
|
|
664
|
+
decisionDeadlineTimer = null;
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
const cancelActiveDecision = () => {
|
|
668
|
+
clearDecisionTimers();
|
|
669
|
+
activeDecision = undefined;
|
|
670
|
+
};
|
|
671
|
+
const isDecisionSnapshotCurrent = (view, decision) => {
|
|
672
|
+
const {
|
|
673
|
+
state
|
|
674
|
+
} = view;
|
|
675
|
+
return state.selection.empty && state.selection.from === decision.position && getTextBeforeCursor(state) === decision.textBefore;
|
|
676
|
+
};
|
|
677
|
+
const isDecisionCurrent = (view, decision) => {
|
|
678
|
+
var _activeDecision;
|
|
679
|
+
return ((_activeDecision = activeDecision) === null || _activeDecision === void 0 ? void 0 : _activeDecision.revision) === decision.revision && isDecisionSnapshotCurrent(view, decision);
|
|
680
|
+
};
|
|
681
|
+
const expireActiveDecision = decision => {
|
|
682
|
+
var _activeDecision2;
|
|
683
|
+
if (((_activeDecision2 = activeDecision) === null || _activeDecision2 === void 0 ? void 0 : _activeDecision2.revision) !== decision.revision) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
cancelActiveDecision();
|
|
687
|
+
};
|
|
688
|
+
const evaluateActiveDecision = (view, revision) => {
|
|
689
|
+
const decision = activeDecision;
|
|
690
|
+
if (!decision || decision.revision !== revision || !isDecisionCurrent(view, decision)) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (performance.now() > decision.deadlineAt) {
|
|
694
|
+
expireActiveDecision(decision);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
decision.evaluations++;
|
|
698
|
+
try {
|
|
699
|
+
const {
|
|
700
|
+
state
|
|
701
|
+
} = view;
|
|
702
|
+
const {
|
|
703
|
+
selection
|
|
704
|
+
} = state;
|
|
494
705
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
706
|
+
// Suppress suggestions when the cursor is mid-word — only offer
|
|
707
|
+
// completions when the cursor is at the trailing edge of a token.
|
|
708
|
+
const nodeAfter = selection.$from.nodeAfter;
|
|
709
|
+
const charAfterCursor = nodeAfter !== null && nodeAfter !== void 0 && nodeAfter.isText ? getLeadingTextCharacter(nodeAfter.text) : undefined;
|
|
710
|
+
if (charAfterCursor && isMidWordCharacter(charAfterCursor)) {
|
|
711
|
+
cancelActiveDecision();
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (decision.textBefore === dismissedContext) {
|
|
715
|
+
cancelActiveDecision();
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
dismissedContext = null;
|
|
508
719
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
720
|
+
// Two characters are enough to prefetch the canonical shortlist. The
|
|
721
|
+
// predictor itself withholds display until the three-character gate.
|
|
722
|
+
if (decision.textBefore.trim().length < 2) {
|
|
723
|
+
cancelActiveDecision();
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const prediction = predict(decision.textBefore);
|
|
727
|
+
if (!prediction || prediction.text.length === 0) {
|
|
728
|
+
// Remain in `collecting` until an async signal arrives or the hard
|
|
729
|
+
// deadline expires. We never display a provisional fallback.
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const readyAtMs = performance.now() - decision.startedAt;
|
|
733
|
+
if (readyAtMs > GHOST_DECISION_BUDGET_MS) {
|
|
734
|
+
cancelActiveDecision();
|
|
735
|
+
if (isAutocompleteDebugEnabled()) {
|
|
736
|
+
// eslint-disable-next-line no-console
|
|
737
|
+
console.log('%c[CTC:budget]%c fully gated after render deadline (render suppressed)', CTC_STYLES.section, CTC_STYLES.body, {
|
|
738
|
+
revision: decision.revision,
|
|
739
|
+
readyAtMs,
|
|
740
|
+
surface: prediction.surface
|
|
741
|
+
});
|
|
517
742
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const suggestion = showGhostText(view, prediction, decision.position, decision.revision, decision.startedAt);
|
|
746
|
+
if (!suggestion) {
|
|
747
|
+
cancelActiveDecision();
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
cancelActiveDecision();
|
|
751
|
+
if (isAutocompleteDebugEnabled()) {
|
|
752
|
+
// eslint-disable-next-line no-console
|
|
753
|
+
console.log('%c[CTC:decision]%c committed immutable ghost', CTC_STYLES.brand, CTC_STYLES.body, {
|
|
754
|
+
revision: suggestion.revision,
|
|
755
|
+
surface: suggestion.surface,
|
|
756
|
+
ghostText: suggestion.ghostText,
|
|
757
|
+
evidenceTier: suggestion.evidenceTier,
|
|
758
|
+
rankScore: suggestion.rankScore,
|
|
759
|
+
winnerMargin: suggestion.winnerMargin,
|
|
760
|
+
decisionLatencyMs: suggestion.decisionLatencyMs,
|
|
761
|
+
budgetMs: GHOST_DECISION_BUDGET_MS,
|
|
762
|
+
typedPrefixLength: getTrailingSurfacePrefixLength(decision.textBefore),
|
|
763
|
+
evaluations: decision.evaluations,
|
|
764
|
+
asyncSignals: decision.asyncSignals,
|
|
765
|
+
readyWithinBudget: suggestion.decisionLatencyMs <= GHOST_DECISION_BUDGET_MS
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
if (suggestion.ghostText !== lastShownGhostText) {
|
|
769
|
+
var _api$analytics5;
|
|
770
|
+
lastShownGhostText = suggestion.ghostText;
|
|
771
|
+
api === null || api === void 0 ? void 0 : (_api$analytics5 = api.analytics) === null || _api$analytics5 === void 0 ? void 0 : _api$analytics5.actions.fireAnalyticsEvent({
|
|
772
|
+
action: ACTION.SUGGESTION_VIEWED,
|
|
773
|
+
actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
|
|
774
|
+
eventType: EVENT_TYPE.TRACK,
|
|
775
|
+
attributes: {
|
|
776
|
+
completionSource: getCompletionSource(),
|
|
777
|
+
surface
|
|
536
778
|
}
|
|
537
|
-
}
|
|
538
|
-
} catch (error) {
|
|
539
|
-
logException(error, {
|
|
540
|
-
location: 'editor-plugin-autocomplete/schedulePrediction'
|
|
541
779
|
});
|
|
542
780
|
}
|
|
543
|
-
}
|
|
781
|
+
} catch (error) {
|
|
782
|
+
cancelActiveDecision();
|
|
783
|
+
logException(error, {
|
|
784
|
+
location: 'editor-plugin-autocomplete/evaluateActiveDecision'
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
const beginPredictionDecision = view => {
|
|
789
|
+
cancelActiveDecision();
|
|
790
|
+
const {
|
|
791
|
+
state
|
|
792
|
+
} = view;
|
|
793
|
+
if (!state.selection.empty) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const startedAt = performance.now();
|
|
797
|
+
const textBefore = getTextBeforeCursor(state);
|
|
798
|
+
const revision = ++decisionRevision;
|
|
799
|
+
activeDecision = {
|
|
800
|
+
asyncSignals: 0,
|
|
801
|
+
deadlineAt: startedAt + GHOST_DECISION_BUDGET_MS,
|
|
802
|
+
evaluations: 0,
|
|
803
|
+
position: state.selection.from,
|
|
804
|
+
revision,
|
|
805
|
+
startedAt,
|
|
806
|
+
textBefore
|
|
807
|
+
};
|
|
808
|
+
if (isAutocompleteDebugEnabled()) {
|
|
809
|
+
// eslint-disable-next-line no-console
|
|
810
|
+
console.log('%c[CTC:decision]%c collecting', CTC_STYLES.brand, CTC_STYLES.body, {
|
|
811
|
+
revision,
|
|
812
|
+
budgetMs: GHOST_DECISION_BUDGET_MS,
|
|
813
|
+
coalesceMs: PREDICTION_COALESCE_MS,
|
|
814
|
+
typedPrefixLength: getTrailingSurfacePrefixLength(activeDecision.textBefore),
|
|
815
|
+
textBefore: activeDecision.textBefore.slice(-80)
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
debounceTimer = setTimeout(() => {
|
|
819
|
+
debounceTimer = null;
|
|
820
|
+
evaluateActiveDecision(view, revision);
|
|
821
|
+
}, PREDICTION_COALESCE_MS);
|
|
822
|
+
decisionDeadlineTimer = setTimeout(() => {
|
|
823
|
+
var _activeDecision3;
|
|
824
|
+
decisionDeadlineTimer = null;
|
|
825
|
+
if (((_activeDecision3 = activeDecision) === null || _activeDecision3 === void 0 ? void 0 : _activeDecision3.revision) === revision) {
|
|
826
|
+
const expiredDecision = activeDecision;
|
|
827
|
+
if (isAutocompleteDebugEnabled()) {
|
|
828
|
+
const elapsedMs = performance.now() - expiredDecision.startedAt;
|
|
829
|
+
const predictionDebug = getLastPredictionDebug();
|
|
830
|
+
const matchingPredictionDebug = (predictionDebug === null || predictionDebug === void 0 ? void 0 : predictionDebug.textBefore) === expiredDecision.textBefore ? predictionDebug : null;
|
|
831
|
+
const abstentionPrefix = '— abstain: ';
|
|
832
|
+
const predictorDecision = matchingPredictionDebug === null || matchingPredictionDebug === void 0 ? void 0 : matchingPredictionDebug.decision;
|
|
833
|
+
const predictorReason = predictorDecision !== null && predictorDecision !== void 0 && predictorDecision.startsWith(abstentionPrefix) ? predictorDecision.slice(abstentionPrefix.length) : predictorDecision;
|
|
834
|
+
const decisionMessage = predictorReason && !(matchingPredictionDebug !== null && matchingPredictionDebug !== void 0 && matchingPredictionDebug.awaitingAsyncEvidence) ? `abstain: ${predictorReason}` : `abstain: ${GHOST_DECISION_BUDGET_MS}ms decision budget expired${predictorReason ? ` · ${predictorReason}` : ''}`;
|
|
835
|
+
// eslint-disable-next-line no-console
|
|
836
|
+
console.log(`%c[CTC:decision]%c ${decisionMessage}`, CTC_STYLES.brand, CTC_STYLES.body, {
|
|
837
|
+
revision,
|
|
838
|
+
budgetMs: GHOST_DECISION_BUDGET_MS,
|
|
839
|
+
elapsedMs,
|
|
840
|
+
typedPrefixLength: getTrailingSurfacePrefixLength(expiredDecision.textBefore),
|
|
841
|
+
evaluations: expiredDecision.evaluations,
|
|
842
|
+
asyncSignals: expiredDecision.asyncSignals,
|
|
843
|
+
textBefore: expiredDecision.textBefore.slice(-80),
|
|
844
|
+
predictorDecision: predictorReason !== null && predictorReason !== void 0 ? predictorReason : null
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
expireActiveDecision(expiredDecision);
|
|
848
|
+
}
|
|
849
|
+
}, GHOST_DECISION_BUDGET_MS);
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
// Async semantic/canonical results may complete the active decision, but they
|
|
853
|
+
// never create a new decision or replace an already committed ghost.
|
|
854
|
+
reschedulePredictionOnSlowLaneResult = signal => {
|
|
855
|
+
if (destroyed || !currentView) {
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (!activeDecision) {
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const revision = activeDecision.revision;
|
|
862
|
+
const elapsedMs = performance.now() - activeDecision.startedAt;
|
|
863
|
+
const remainingMs = activeDecision.deadlineAt - performance.now();
|
|
864
|
+
if (remainingMs < 0) {
|
|
865
|
+
const expiredDecision = activeDecision;
|
|
866
|
+
expireActiveDecision(expiredDecision);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
activeDecision.asyncSignals++;
|
|
870
|
+
if (isAutocompleteDebugVerbose()) {
|
|
871
|
+
// eslint-disable-next-line no-console
|
|
872
|
+
console.log('%c[CTC:readiness]%c async evidence signal', CTC_STYLES.section, CTC_STYLES.body, {
|
|
873
|
+
...signal,
|
|
874
|
+
revision,
|
|
875
|
+
elapsedMs,
|
|
876
|
+
remainingMs: Math.max(0, remainingMs),
|
|
877
|
+
typedPrefixLength: getTrailingSurfacePrefixLength(activeDecision.textBefore),
|
|
878
|
+
withinBudget: remainingMs >= 0
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
if (debounceTimer) {
|
|
882
|
+
clearTimeout(debounceTimer);
|
|
883
|
+
}
|
|
884
|
+
debounceTimer = setTimeout(() => {
|
|
885
|
+
debounceTimer = null;
|
|
886
|
+
if (currentView) {
|
|
887
|
+
evaluateActiveDecision(currentView, revision);
|
|
888
|
+
}
|
|
889
|
+
}, 0);
|
|
544
890
|
};
|
|
545
891
|
const maybeUpdateSessionFrequency = (view, prevState) => {
|
|
546
892
|
const newText = getTextBeforeCursor(view.state);
|
|
@@ -579,22 +925,14 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
579
925
|
};
|
|
580
926
|
}
|
|
581
927
|
|
|
582
|
-
// A new prediction is scheduled from view.update
|
|
928
|
+
// A new prediction is scheduled from view.update either way; the
|
|
929
|
+
// surviving ghost only covers the gap until it commits.
|
|
583
930
|
if (tr.docChanged) {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
ghostText: '',
|
|
587
|
-
ghostPosition: -1,
|
|
588
|
-
decorationSet: DecorationSet.empty
|
|
589
|
-
};
|
|
931
|
+
var _advanceGhostThroughT;
|
|
932
|
+
return (_advanceGhostThroughT = advanceGhostThroughTypedCharacter(tr, pluginState)) !== null && _advanceGhostThroughT !== void 0 ? _advanceGhostThroughT : clearGhostState(pluginState);
|
|
590
933
|
}
|
|
591
934
|
if (tr.selectionSet && pluginState.ghostText) {
|
|
592
|
-
return
|
|
593
|
-
...pluginState,
|
|
594
|
-
ghostText: '',
|
|
595
|
-
ghostPosition: -1,
|
|
596
|
-
decorationSet: DecorationSet.empty
|
|
597
|
-
};
|
|
935
|
+
return clearGhostState(pluginState);
|
|
598
936
|
}
|
|
599
937
|
return pluginState;
|
|
600
938
|
}
|
|
@@ -607,22 +945,25 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
607
945
|
},
|
|
608
946
|
handleKeyDown: keydownHandler({
|
|
609
947
|
Tab: (state, dispatch) => {
|
|
610
|
-
return acceptGhostTextWithAnalytics(state, dispatch,
|
|
948
|
+
return acceptGhostTextWithAnalytics(state, dispatch, suggestion => {
|
|
611
949
|
justAccepted = true;
|
|
612
950
|
lastShownGhostText = '';
|
|
613
|
-
|
|
951
|
+
cancelActiveDecision();
|
|
952
|
+
fireSuggestionInsertedAnalytics(suggestion);
|
|
614
953
|
});
|
|
615
954
|
},
|
|
616
955
|
ArrowRight: (state, dispatch) => {
|
|
617
|
-
return acceptGhostTextWithAnalytics(state, dispatch,
|
|
956
|
+
return acceptGhostTextWithAnalytics(state, dispatch, suggestion => {
|
|
618
957
|
justAccepted = true;
|
|
619
958
|
lastShownGhostText = '';
|
|
620
|
-
|
|
959
|
+
cancelActiveDecision();
|
|
960
|
+
fireSuggestionInsertedAnalytics(suggestion);
|
|
621
961
|
});
|
|
622
962
|
},
|
|
623
963
|
Escape: (state, dispatch) => {
|
|
624
964
|
const didClear = clearGhostText(state, dispatch);
|
|
625
965
|
if (didClear) {
|
|
966
|
+
cancelActiveDecision();
|
|
626
967
|
dismissedContext = getTextBeforeCursor(state);
|
|
627
968
|
fireSuggestionDismissedAnalytics('escape');
|
|
628
969
|
}
|
|
@@ -637,6 +978,7 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
637
978
|
const pluginState = autocompletePluginKey.getState(view.state);
|
|
638
979
|
if (pluginState !== null && pluginState !== void 0 && pluginState.ghostText) {
|
|
639
980
|
clearGhostText(view.state, view.dispatch);
|
|
981
|
+
cancelActiveDecision();
|
|
640
982
|
fireSuggestionDismissedAnalytics('blur');
|
|
641
983
|
}
|
|
642
984
|
return false;
|
|
@@ -648,6 +990,7 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
648
990
|
if (!event.target.closest('[data-autocomplete-ghost="true"]')) {
|
|
649
991
|
const didClear = clearGhostText(view.state, view.dispatch);
|
|
650
992
|
if (didClear) {
|
|
993
|
+
cancelActiveDecision();
|
|
651
994
|
dismissedContext = getTextBeforeCursor(view.state);
|
|
652
995
|
lastShownGhostText = '';
|
|
653
996
|
fireSuggestionDismissedAnalytics('click');
|
|
@@ -655,10 +998,11 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
655
998
|
return false;
|
|
656
999
|
}
|
|
657
1000
|
event.preventDefault();
|
|
658
|
-
const accepted = acceptGhostTextWithAnalytics(view.state, view.dispatch,
|
|
1001
|
+
const accepted = acceptGhostTextWithAnalytics(view.state, view.dispatch, suggestion => {
|
|
659
1002
|
justAccepted = true;
|
|
660
1003
|
lastShownGhostText = '';
|
|
661
|
-
|
|
1004
|
+
cancelActiveDecision();
|
|
1005
|
+
fireSuggestionInsertedAnalytics(suggestion);
|
|
662
1006
|
});
|
|
663
1007
|
if (accepted) {
|
|
664
1008
|
view.focus();
|
|
@@ -666,18 +1010,11 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
666
1010
|
return accepted;
|
|
667
1011
|
},
|
|
668
1012
|
focus: () => {
|
|
669
|
-
var _options$
|
|
1013
|
+
var _options$useLocalMode2, _options$useLocalMode3;
|
|
670
1014
|
if (!isAutocompleteEnabled) {
|
|
671
1015
|
return false;
|
|
672
1016
|
}
|
|
673
|
-
|
|
674
|
-
isLocalLLM: (_options$useLocalMode = options === null || options === void 0 ? void 0 : options.useLocalModel) !== null && _options$useLocalMode !== void 0 ? _options$useLocalMode : false,
|
|
675
|
-
surface
|
|
676
|
-
}).catch(error => {
|
|
677
|
-
logException(error, {
|
|
678
|
-
location: 'editor-plugin-autocomplete/loadDefaultVocabulary'
|
|
679
|
-
});
|
|
680
|
-
});
|
|
1017
|
+
void ensureVocabularyReady();
|
|
681
1018
|
loadVectorsAsync({
|
|
682
1019
|
isLocalLLM: (_options$useLocalMode2 = options === null || options === void 0 ? void 0 : options.useLocalModel) !== null && _options$useLocalMode2 !== void 0 ? _options$useLocalMode2 : false,
|
|
683
1020
|
surface
|
|
@@ -733,9 +1070,7 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
733
1070
|
// Snapshot the post-acceptance text so follow-up transactions hit
|
|
734
1071
|
// the dismissedContext guard and abort until the user types again.
|
|
735
1072
|
dismissedContext = getTextBeforeCursor(view.state);
|
|
736
|
-
|
|
737
|
-
clearTimeout(debounceTimer);
|
|
738
|
-
}
|
|
1073
|
+
cancelActiveDecision();
|
|
739
1074
|
return;
|
|
740
1075
|
}
|
|
741
1076
|
maybeUpdateSessionFrequency(view, prevState);
|
|
@@ -760,7 +1095,9 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
760
1095
|
}
|
|
761
1096
|
}
|
|
762
1097
|
}
|
|
763
|
-
|
|
1098
|
+
beginPredictionDecision(view);
|
|
1099
|
+
} else if (!prevState.selection.eq(view.state.selection)) {
|
|
1100
|
+
cancelActiveDecision();
|
|
764
1101
|
}
|
|
765
1102
|
},
|
|
766
1103
|
destroy: () => {
|
|
@@ -769,14 +1106,14 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
769
1106
|
currentView = null;
|
|
770
1107
|
(_unsubscribeFromConte = unsubscribeFromContextUpdates) === null || _unsubscribeFromConte === void 0 ? void 0 : _unsubscribeFromConte();
|
|
771
1108
|
unsubscribeFromContextUpdates = undefined;
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
}
|
|
1109
|
+
cancelActiveDecision();
|
|
1110
|
+
const releasedDefaultClient = clearDefaultSlowLaneClient(slowLaneClient);
|
|
1111
|
+
ctcTag('init', `slow lane teardown · id=${slowLaneClientId} · default=${releasedDefaultClient ? 'released' : 'retained newer owner'}`, releasedDefaultClient ? CTC_STYLES.dim : CTC_STYLES.warn);
|
|
775
1112
|
if (hasDestroy(slowLaneClient)) {
|
|
776
1113
|
slowLaneClient.destroy();
|
|
777
1114
|
}
|
|
778
|
-
|
|
779
|
-
|
|
1115
|
+
pendingSessionContextTexts.clear();
|
|
1116
|
+
sessionIngestedContextTexts.clear();
|
|
780
1117
|
}
|
|
781
1118
|
};
|
|
782
1119
|
}
|