Skip to content

Commit 0c3ec94

Browse files
committed
fix: preserve editor lifecycle and text navigation handoffs
1 parent 0c9cdd7 commit 0c3ec94

3 files changed

Lines changed: 346 additions & 32 deletions

File tree

‎src/CsvEditorProvider.ts‎

Lines changed: 69 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ class CsvEditorController {
6565
private isUpdatingDocument = false;
6666
private isSaving = false;
6767
private pendingOperation: Promise<void> = Promise.resolve();
68+
private isDisposed = false;
69+
private defaultEditorHandoff: Promise<void> | undefined;
6870
private currentWebviewPanel: vscode.WebviewPanel | undefined;
6971
private document!: vscode.TextDocument;
7072
private separatorCache: { version: number; configKey: string; separator: string } | undefined;
@@ -81,6 +83,24 @@ class CsvEditorController {
8183
_token: vscode.CancellationToken
8284
): Promise<void> {
8385
this.document = document;
86+
this.currentWebviewPanel = webviewPanel;
87+
let changeDocumentSubscription: vscode.Disposable | undefined;
88+
let saveDocumentSubscription: vscode.Disposable | undefined;
89+
// Remote file checks can outlive the preview tab that requested them.
90+
// Observe disposal before awaiting anything so a closed panel stays closed.
91+
webviewPanel.onDidDispose(() => {
92+
this.isDisposed = true;
93+
changeDocumentSubscription?.dispose();
94+
saveDocumentSubscription?.dispose();
95+
CsvEditorProvider.editors = CsvEditorProvider.editors.filter(ed => ed !== this);
96+
if (CsvEditorProvider.currentActive === this) {
97+
CsvEditorProvider.currentActive = undefined;
98+
}
99+
this.currentWebviewPanel = undefined;
100+
});
101+
if (_token.isCancellationRequested) {
102+
return;
103+
}
84104

85105
const config = vscode.workspace.getConfiguration('csv', this.document.uri);
86106
if (!config.get<boolean>('enabled', true)) {
@@ -90,17 +110,20 @@ class CsvEditorController {
90110
}
91111

92112
const proceed = await this.confirmLargeFileOpen(config, webviewPanel, _token);
93-
if (!proceed) {
113+
if (!proceed || this.isDisposed || _token.isCancellationRequested) {
114+
return;
115+
}
116+
// csv.enabled may have changed while a remote stat or the size prompt was pending.
117+
if (!vscode.workspace.getConfiguration('csv', document.uri).get<boolean>('enabled', true)) {
118+
await this.openWithDefaultEditorAndClose(webviewPanel, document.uri);
94119
return;
95120
}
96121

97-
this.currentWebviewPanel = webviewPanel;
98122
CsvEditorProvider.editors.push(this);
99123

100124
webviewPanel.webview.options = {
101125
enableScripts: true,
102-
// Use file path for compatibility with older VS Code types (no Uri.joinPath)
103-
localResourceRoots: [vscode.Uri.file(path.join(this.context.extensionPath, 'media'))]
126+
localResourceRoots: [this.getMediaUri()]
104127
};
105128

106129
this.refreshDiffContext(webviewPanel);
@@ -124,6 +147,8 @@ class CsvEditorController {
124147
}
125148
e.webviewPanel.webview.postMessage({ type: 'focus' });
126149
CsvEditorProvider.currentActive = this;
150+
} else if (CsvEditorProvider.currentActive === this) {
151+
CsvEditorProvider.currentActive = undefined;
127152
}
128153
});
129154

@@ -190,7 +215,7 @@ class CsvEditorController {
190215
}
191216
}));
192217

193-
const changeDocumentSubscription = vscode.workspace.onDidChangeTextDocument(e => {
218+
changeDocumentSubscription = vscode.workspace.onDidChangeTextDocument(e => {
194219
if (
195220
e.document.uri.toString() === document.uri.toString() &&
196221
!this.isUpdatingDocument &&
@@ -200,20 +225,14 @@ class CsvEditorController {
200225
}
201226
});
202227

203-
const saveDocumentSubscription = vscode.workspace.onWillSaveTextDocument(e => {
228+
saveDocumentSubscription = vscode.workspace.onWillSaveTextDocument(e => {
204229
if (e.document.uri.toString() === document.uri.toString() && !this.isSaving) {
205230
// Native File > Save can run while the webview's blur edit is applying.
206231
// The custom save command is already in this queue and must not await itself.
207232
e.waitUntil(this.pendingOperation);
208233
}
209234
});
210235

211-
webviewPanel.onDidDispose(() => {
212-
changeDocumentSubscription.dispose();
213-
saveDocumentSubscription.dispose();
214-
CsvEditorProvider.editors = CsvEditorProvider.editors.filter(ed => ed !== this);
215-
this.currentWebviewPanel = undefined;
216-
});
217236
}
218237

219238
private enqueueOperation(operation: () => void | Promise<void>): Promise<void> {
@@ -252,16 +271,30 @@ class CsvEditorController {
252271
}
253272

254273
private async openWithDefaultEditorAndClose(webviewPanel: vscode.WebviewPanel, uri: vscode.Uri): Promise<void> {
255-
try {
256-
const opts: any = {
257-
viewColumn: webviewPanel.viewColumn,
258-
preserveFocus: !webviewPanel.active,
259-
preview: webviewPanel.active ? webviewPanel.active : false
260-
};
261-
await vscode.commands.executeCommand('vscode.openWith', uri, 'default', opts);
262-
} finally {
263-
try { webviewPanel.dispose(); } catch {}
274+
if (this.isDisposed) {
275+
return;
264276
}
277+
if (!this.defaultEditorHandoff) {
278+
this.defaultEditorHandoff = (async () => {
279+
try {
280+
// Search can already have revealed the text editor. Opening it again
281+
// loses its destination, so leave an existing editor in this group alone.
282+
const existing = vscode.window.visibleTextEditors.find(editor =>
283+
editor.document.uri.toString() === uri.toString() &&
284+
editor.viewColumn === webviewPanel.viewColumn
285+
);
286+
if (!existing) {
287+
await vscode.commands.executeCommand('vscode.openWith', uri, 'default', {
288+
viewColumn: webviewPanel.viewColumn,
289+
preserveFocus: !webviewPanel.active
290+
});
291+
}
292+
} finally {
293+
try { webviewPanel.dispose(); } catch {}
294+
}
295+
})();
296+
}
297+
await this.defaultEditorHandoff;
265298
}
266299

267300
private async confirmLargeFileOpen(
@@ -319,8 +352,12 @@ class CsvEditorController {
319352
public refresh() {
320353
const config = vscode.workspace.getConfiguration('csv', this.document.uri);
321354
if (!config.get<boolean>('enabled', true)) {
322-
this.currentWebviewPanel?.dispose();
323-
vscode.commands.executeCommand('vscode.openWith', this.document.uri, 'default');
355+
const panel = this.currentWebviewPanel;
356+
if (panel) {
357+
void this.openWithDefaultEditorAndClose(panel, this.document.uri).catch(error => {
358+
console.error('CSV: failed to reopen the text editor', error);
359+
});
360+
}
324361
} else {
325362
if (this.currentWebviewPanel) {
326363
this.forceReload();
@@ -1690,6 +1727,12 @@ class CsvEditorController {
16901727
return !matches;
16911728
}
16921729

1730+
private getMediaUri(...segments: string[]): vscode.Uri {
1731+
return this.context.extensionUri.with({
1732+
path: path.posix.join(this.context.extensionUri.path, 'media', ...segments)
1733+
});
1734+
}
1735+
16931736
private wrapHtml(args: {
16941737
webview: vscode.Webview;
16951738
nonce: string;
@@ -1707,9 +1750,8 @@ class CsvEditorController {
17071750
}): string {
17081751
const { webview, nonce, fontFamily, fontSize, cellPadding, separator, tableHtml, chunksJson, extraColumnColorCss, nextChunkStart, hasRemoteChunks, mouseWheelZoomEnabled, mouseWheelZoomInvert } = args;
17091752
const isDark = vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark;
1710-
// Build script URI using file path for compatibility (older APIs may lack Uri.joinPath)
17111753
const scriptUri = webview.asWebviewUri(
1712-
vscode.Uri.file(path.join(this.context.extensionPath, 'media', 'main.js'))
1754+
this.getMediaUri('main.js')
17131755
);
17141756

17151757
// Safe separator transport (assumes single character; see assumptions)
@@ -2539,17 +2581,12 @@ export class CsvEditorProvider implements vscode.CustomTextEditorProvider {
25392581
): Promise<void> {
25402582
console.log(`CSV(reg): creating controller for ${document.uri.toString()}`);
25412583
const controller = new CsvEditorController(this.context);
2542-
// Track active controller
2543-
webviewPanel.onDidChangeViewState(e => {
2544-
if (e.webviewPanel.active) {
2545-
CsvEditorProvider.currentActive = controller;
2546-
}
2547-
});
25482584
await controller.resolveCustomTextEditor(document, webviewPanel, _token);
25492585
}
25502586

25512587
public static getActiveProvider(): CsvEditorController | undefined {
2552-
return CsvEditorProvider.currentActive || CsvEditorProvider.editors.find(ed => ed.isActive());
2588+
const current = CsvEditorProvider.currentActive;
2589+
return current?.isActive() ? current : CsvEditorProvider.editors.find(ed => ed.isActive());
25532590
}
25542591

25552592
public static getHiddenRowsForUri(context: vscode.ExtensionContext, uri: vscode.Uri): number {

‎src/test/edit-integrity-harness.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class TestUri {
88
get fsPath() { return this.path; }
99
toString() { return `${this.scheme}://${this.path}`; }
1010
static file(filePath: string) { return new TestUri(filePath); }
11+
with(changes: { path: string }) { return new TestUri(changes.path, this.scheme); }
1112
}
1213

1314
let activeHarness: any;
@@ -77,6 +78,7 @@ export async function createEditorHarness(options: {
7778
};
7879
const context = {
7980
extensionPath: process.cwd(),
81+
extensionUri: TestUri.file(process.cwd()),
8082
workspaceState: {
8183
values: {} as Record<string, unknown>,
8284
get(key: string, fallback: unknown) { return this.values[key] ?? fallback; },

0 commit comments

Comments
 (0)