Skip to content

Commit b4dc6b6

Browse files
committed
fix scan cancellation lifecycle
1 parent ad859c8 commit b4dc6b6

3 files changed

Lines changed: 47 additions & 10 deletions

File tree

VSIX/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

VSIX/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "snow-cli",
33
"displayName": "%extension.displayName%",
44
"description": "%extension.description%",
5-
"version": "0.4.24",
5+
"version": "0.4.25",
66
"publisher": "mufasa",
77
"icon": "snow.png",
88
"repository": {

VSIX/src/nextEdit/engine.ts

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -258,11 +258,27 @@ export class NextEditEngine implements vscode.Disposable {
258258
await this.advance();
259259
}
260260

261-
/** Esc: dismiss. */
261+
/** Esc: dismiss. Also cancels any in-flight scan even before a session is established. */
262262
public dismiss(reason = 'user'): void {
263-
if (!this.session) return;
263+
const hadScan = !!this.scanCts;
264+
if (!this.session) {
265+
// No active candidate session, but a scan may still be in-flight
266+
// (e.g. the user pressed Esc, or Next Edit was just disabled, while
267+
// the AI request is mid-flight). Cancel the scan and clear the
268+
// status bar's spinner so the loading state doesn't get stuck.
269+
if (hadScan) {
270+
log(`scan cancelled (no session): ${reason}`);
271+
this.scanCts?.cancel();
272+
this.scanCts?.dispose();
273+
this.scanCts = undefined;
274+
this.statusBar.setStatus('idle');
275+
}
276+
return;
277+
}
264278
log(`session dismissed: ${reason}`);
265279
this.scanCts?.cancel();
280+
this.scanCts?.dispose();
281+
this.scanCts = undefined;
266282
this.session = undefined;
267283
this.deco.clear();
268284
this.statusBar.setStatus('idle');
@@ -272,20 +288,41 @@ export class NextEditEngine implements vscode.Disposable {
272288
private async onEdit(edit: RecentEdit, manual = false): Promise<void> {
273289
if (!this.config.enabled && !manual) return;
274290
this.scanCts?.cancel();
275-
this.scanCts = new vscode.CancellationTokenSource();
276-
const token = this.scanCts.token;
291+
this.scanCts?.dispose();
292+
const cts = new vscode.CancellationTokenSource();
293+
this.scanCts = cts;
294+
const token = cts.token;
277295

278296
this.statusBar.setStatus('scanning');
279297
let candidates: Candidate[];
280298
try {
281299
candidates = await this.finder.find(edit, token);
282300
} catch (err) {
283301
log(`scan error: ${(err as Error)?.message ?? err}`);
284-
this.statusBar.setMessage('scan failed');
285-
this.statusBar.setStatus('idle');
302+
// Only touch the status bar if our scan is still the active one.
303+
// A newer onEdit may have already replaced us and set its own state.
304+
if (this.scanCts === cts) {
305+
this.scanCts = undefined;
306+
this.statusBar.setMessage('scan failed');
307+
this.statusBar.setStatus('idle');
308+
}
286309
return;
287310
}
288-
if (token.isCancellationRequested) return;
311+
if (token.isCancellationRequested) {
312+
// We got cancelled (new edit, dismiss, or dispose). If a newer scan
313+
// has already taken over (this.scanCts !== cts), leave the status
314+
// alone — the newer scan owns it. Otherwise restore idle so the
315+
// spinner doesn't stay forever after a silent abort.
316+
if (this.scanCts === cts) {
317+
this.scanCts = undefined;
318+
this.statusBar.setStatus('idle');
319+
}
320+
return;
321+
}
322+
// Scan finished naturally; release ownership of the cts.
323+
if (this.scanCts === cts) {
324+
this.scanCts = undefined;
325+
}
289326

290327
if (candidates.length === 0) {
291328
log('no candidates');

0 commit comments

Comments
 (0)