Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/inlineEdits/node/speculativeRequestManager.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { DocumentId } from '../../../platform/inlineEdits/common/dataTypes/documentId';
7
import { StatelessNextEditRequest } from '../../../platform/inlineEdits/common/statelessNextEditProvider';
8
import { ILogger } from '../../../platform/log/common/logService';
9
import { Disposable } from '../../../util/vs/base/common/lifecycle';
10
import { CachedOrRebasedEdit } from './nextEditCache';
11
import { NextEditResult } from './nextEditResult';
12
13
/**
14
* Reasons why a speculative request was cancelled. Recorded on the request's
15
* log context so each cancellation has an attributable cause.
16
*/
17
export const enum SpeculativeCancelReason {
18
19
/** The originating suggestion was rejected by the user. */
20
Rejected = 'rejected',
21
22
/** The originating suggestion was dismissed without being superseded. */
23
IgnoredDismissed = 'ignoredDismissed',
24
25
/** A new fetch is starting whose `(docId, postEditContent)` doesn't match. */
26
Superseded = 'superseded',
27
28
/** A newer speculative is being installed in this slot. */
29
Replaced = 'replaced',
30
31
/** The user's edits moved off the type-through trajectory toward `postEditContent`. */
32
DivergedFromTrajectoryForm = 'divergedFromTrajectoryForm',
33
DivergedFromTrajectoryPrefix = 'divergedFromTrajectoryPrefix',
34
DivergedFromTrajectoryMiddle = 'divergedFromTrajectoryMiddle',
35
DivergedFromTrajectorySuffix = 'divergedFromTrajectorySuffix',
36
37
/** `clearCache()` was invoked. */
38
CacheCleared = 'cacheCleared',
39
40
/** The target document was removed from the workspace. */
41
DocumentClosed = 'documentClosed',
42
43
/** The provider was disposed. */
44
Disposed = 'disposed',
45
}
46
47
export interface SpeculativePendingRequest {
48
readonly request: StatelessNextEditRequest<CachedOrRebasedEdit>;
49
readonly docId: DocumentId;
50
readonly postEditContent: string;
51
/** preEditDocument[0..editStart] — the doc text before the edit window. */
52
readonly trajectoryPrefix: string;
53
/** preEditDocument[editEnd..] — the doc text after the edit window. */
54
readonly trajectorySuffix: string;
55
/** The replacement text the user would type to reach `postEditContent`. */
56
readonly trajectoryNewText: string;
57
}
58
59
export interface ScheduledSpeculativeRequest {
60
readonly suggestion: NextEditResult;
61
readonly headerRequestId: string;
62
}
63
64
/**
65
* Owns the lifecycle of NES speculative requests:
66
*
67
* - the in-flight `pending` speculative (the bet on a specific post-accept document state)
68
* - the `scheduled` speculative deferred until its originating stream completes
69
*
70
* Centralizes cancellation with typed reasons so every triggered cancellation
71
* (reject, supersede, doc-close, trajectory divergence, dispose, ...) goes through
72
* one path and is logged on the request's log context.
73
*/
74
export class SpeculativeRequestManager extends Disposable {
75
76
private _pending: SpeculativePendingRequest | null = null;
77
private _scheduled: ScheduledSpeculativeRequest | null = null;
78
79
constructor(private readonly _logger: ILogger) {
80
super();
81
}
82
83
get pending(): SpeculativePendingRequest | null {
84
return this._pending;
85
}
86
87
/** Replaces the current pending speculative; cancels the prior one as `Replaced`. */
88
setPending(req: SpeculativePendingRequest): void {
89
if (this._pending && this._pending.request !== req.request) {
90
this._cancelPending(SpeculativeCancelReason.Replaced);
91
}
92
this._pending = req;
93
}
94
95
/** Detaches the pending speculative without cancelling — caller is consuming it. */
96
consumePending(): void {
97
this._pending = null;
98
}
99
100
schedule(s: ScheduledSpeculativeRequest): void {
101
this._scheduled = s;
102
}
103
104
clearScheduled(): void {
105
this._scheduled = null;
106
}
107
108
/**
109
* Removes and returns the scheduled entry iff its `headerRequestId` matches.
110
* Used by the streaming path so that each stream only ever consumes its own
111
* schedule, never another stream's.
112
*/
113
consumeScheduled(headerRequestId: string): ScheduledSpeculativeRequest | null {
114
if (this._scheduled?.headerRequestId !== headerRequestId) {
115
return null;
116
}
117
const s = this._scheduled;
118
this._scheduled = null;
119
return s;
120
}
121
122
cancelAll(reason: SpeculativeCancelReason): void {
123
this._scheduled = null;
124
this._cancelPending(reason);
125
}
126
127
/** Cancels the pending speculative iff `(docId, postEditContent)` doesn't match. */
128
cancelIfMismatch(docId: DocumentId, postEditContent: string, reason: SpeculativeCancelReason): void {
129
if (this._pending && (this._pending.docId !== docId || this._pending.postEditContent !== postEditContent)) {
130
this._cancelPending(reason);
131
}
132
}
133
134
/** Cancels the pending and clears any scheduled targeting this document. */
135
onDocumentClosed(docId: DocumentId): void {
136
if (this._scheduled?.suggestion.result?.targetDocumentId === docId) {
137
this._scheduled = null;
138
}
139
if (this._pending?.docId === docId) {
140
this._cancelPending(SpeculativeCancelReason.DocumentClosed);
141
}
142
}
143
144
/**
145
* Trajectory check. The pending speculative is alive iff the current document
146
* value is a *type-through prefix* toward the speculative's `postEditContent`:
147
*
148
* cur === trajectoryPrefix + middle + trajectorySuffix
149
* where middle is some prefix of trajectoryNewText
150
*
151
* If not, the user's edits cannot reach `postEditContent` via continued typing
152
* and the speculative will never be consumed — cancel now.
153
*/
154
onActiveDocumentChanged(docId: DocumentId, currentDocValue: string): void {
155
const p = this._pending;
156
if (!p || p.docId !== docId) {
157
return;
158
}
159
// Cheap structural failure: doc shorter than the unedited frame.
160
if (currentDocValue.length < p.trajectoryPrefix.length + p.trajectorySuffix.length) {
161
this._cancelPending(SpeculativeCancelReason.DivergedFromTrajectoryForm);
162
return;
163
}
164
if (!currentDocValue.startsWith(p.trajectoryPrefix)) {
165
this._cancelPending(SpeculativeCancelReason.DivergedFromTrajectoryPrefix);
166
return;
167
}
168
if (!currentDocValue.endsWith(p.trajectorySuffix)) {
169
this._cancelPending(SpeculativeCancelReason.DivergedFromTrajectorySuffix);
170
return;
171
}
172
const middle = currentDocValue.slice(p.trajectoryPrefix.length, currentDocValue.length - p.trajectorySuffix.length);
173
if (!p.trajectoryNewText.startsWith(middle)) {
174
this._cancelPending(SpeculativeCancelReason.DivergedFromTrajectoryMiddle);
175
}
176
}
177
178
private _cancelPending(reason: SpeculativeCancelReason): void {
179
const p = this._pending;
180
if (!p) {
181
return;
182
}
183
this._pending = null;
184
const headerRequestId = p.request.headerRequestId;
185
this._logger.trace(`cancelling speculative request: ${reason} (headerRequestId=${headerRequestId})`);
186
p.request.logContext.addLog(`speculative request cancelled: ${reason}`);
187
const cts = p.request.cancellationTokenSource;
188
cts.cancel();
189
// Dispose to release the cancel-event listeners that the in-flight
190
// provider call hooked onto the token. Safe even though the runner may
191
// observe cancellation asynchronously — `cancel()` already fired the event.
192
cts.dispose();
193
}
194
195
override dispose(): void {
196
this.cancelAll(SpeculativeCancelReason.Disposed);
197
super.dispose();
198
}
199
}
200
201