Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Open the Command Palette and search for:

Global (Settings UI or `settings.json`):

- `csv.enabled` (boolean, default `true`): Enable/disable the custom editor.
- `csv.enabled` (boolean, default `true`): Enable/disable the custom editor. The existing Toggle Extension command honors an explicit workspace value, otherwise it changes the global value. While off, CSV temporarily routes its four file selectors and existing explicit CSV-editor associations to the native text editor, so Find in Files preserves the destination and selection. Re-enabling restores the previous associations. Other editors, unrelated associations, and intervening user changes are preserved. Routing recovery metadata is stored in VS Code extension state; it contains editor identifiers and patterns, never CSV data. If settings or policy prevent routing, CSV reports the failure instead of claiming the toggle succeeded.
- `csv.fontFamily` (string, default empty): Override font family; falls back to `editor.fontFamily`.
- `csv.fontSize` (number, default `0`): Override font size in px; set to `0` to inherit `editor.fontSize`.
- `csv.mouseWheelZoom` (boolean, default `true`): Enable `Ctrl/Cmd + Mouse Wheel` zooming in the CSV editor.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"Other"
],
"activationEvents": [
"onStartupFinished",
"onLanguage:csv",
"onLanguage:tsv",
"onCommand:csv.toggleExtension",
Expand Down
101 changes: 69 additions & 32 deletions src/CsvEditorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
private isUpdatingDocument = false;
private isSaving = false;
private pendingOperation: Promise<void> = Promise.resolve();
private isDisposed = false;
private defaultEditorHandoff: Promise<void> | undefined;
private currentWebviewPanel: vscode.WebviewPanel | undefined;
private document!: vscode.TextDocument;
private separatorCache: { version: number; configKey: string; separator: string } | undefined;
Expand All @@ -81,6 +83,24 @@
_token: vscode.CancellationToken
): Promise<void> {
this.document = document;
this.currentWebviewPanel = webviewPanel;
let changeDocumentSubscription: vscode.Disposable | undefined;
let saveDocumentSubscription: vscode.Disposable | undefined;
// Remote file checks can outlive the preview tab that requested them.
// Observe disposal before awaiting anything so a closed panel stays closed.
webviewPanel.onDidDispose(() => {
this.isDisposed = true;
changeDocumentSubscription?.dispose();
saveDocumentSubscription?.dispose();
CsvEditorProvider.editors = CsvEditorProvider.editors.filter(ed => ed !== this);
if (CsvEditorProvider.currentActive === this) {
CsvEditorProvider.currentActive = undefined;
}
this.currentWebviewPanel = undefined;
});
if (_token.isCancellationRequested) {
return;
}

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

const proceed = await this.confirmLargeFileOpen(config, webviewPanel, _token);
if (!proceed) {
if (!proceed || this.isDisposed || _token.isCancellationRequested) {
return;
}
// csv.enabled may have changed while a remote stat or the size prompt was pending.
if (!vscode.workspace.getConfiguration('csv', document.uri).get<boolean>('enabled', true)) {
await this.openWithDefaultEditorAndClose(webviewPanel, document.uri);
return;
}

this.currentWebviewPanel = webviewPanel;
CsvEditorProvider.editors.push(this);

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

this.refreshDiffContext(webviewPanel);
Expand All @@ -124,6 +147,8 @@
}
e.webviewPanel.webview.postMessage({ type: 'focus' });
CsvEditorProvider.currentActive = this;
} else if (CsvEditorProvider.currentActive === this) {
CsvEditorProvider.currentActive = undefined;
}
});

Expand Down Expand Up @@ -190,7 +215,7 @@
}
}));

const changeDocumentSubscription = vscode.workspace.onDidChangeTextDocument(e => {
changeDocumentSubscription = vscode.workspace.onDidChangeTextDocument(e => {
if (
e.document.uri.toString() === document.uri.toString() &&
!this.isUpdatingDocument &&
Expand All @@ -200,20 +225,14 @@
}
});

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

webviewPanel.onDidDispose(() => {
changeDocumentSubscription.dispose();
saveDocumentSubscription.dispose();
CsvEditorProvider.editors = CsvEditorProvider.editors.filter(ed => ed !== this);
this.currentWebviewPanel = undefined;
});
}

private enqueueOperation(operation: () => void | Promise<void>): Promise<void> {
Expand Down Expand Up @@ -252,16 +271,30 @@
}

private async openWithDefaultEditorAndClose(webviewPanel: vscode.WebviewPanel, uri: vscode.Uri): Promise<void> {
try {
const opts: any = {
viewColumn: webviewPanel.viewColumn,
preserveFocus: !webviewPanel.active,
preview: webviewPanel.active ? webviewPanel.active : false
};
await vscode.commands.executeCommand('vscode.openWith', uri, 'default', opts);
} finally {
try { webviewPanel.dispose(); } catch {}
if (this.isDisposed) {
return;
}
if (!this.defaultEditorHandoff) {
this.defaultEditorHandoff = (async () => {
try {
// Search can already have revealed the text editor. Opening it again
// loses its destination, so leave an existing editor in this group alone.
const existing = vscode.window.visibleTextEditors.find(editor =>
editor.document.uri.toString() === uri.toString() &&
editor.viewColumn === webviewPanel.viewColumn
);
if (!existing) {
await vscode.commands.executeCommand('vscode.openWith', uri, 'default', {
viewColumn: webviewPanel.viewColumn,
preserveFocus: !webviewPanel.active
});
}
} finally {
try { webviewPanel.dispose(); } catch {}
}
})();
}
await this.defaultEditorHandoff;
}

private async confirmLargeFileOpen(
Expand Down Expand Up @@ -319,8 +352,12 @@
public refresh() {
const config = vscode.workspace.getConfiguration('csv', this.document.uri);
if (!config.get<boolean>('enabled', true)) {
this.currentWebviewPanel?.dispose();
vscode.commands.executeCommand('vscode.openWith', this.document.uri, 'default');
const panel = this.currentWebviewPanel;
if (panel) {
void this.openWithDefaultEditorAndClose(panel, this.document.uri).catch(error => {
console.error('CSV: failed to reopen the text editor', error);
});
}
} else {
if (this.currentWebviewPanel) {
this.forceReload();
Expand All @@ -329,7 +366,7 @@
}

private forceReload() {
if (!this.currentWebviewPanel) return;

Check warning on line 369 in src/CsvEditorProvider.ts

View workflow job for this annotation

GitHub Actions / build

Expected { after 'if' condition
const panel = this.currentWebviewPanel;
// First, blank the DOM to ensure a full script/style reinit on next set
panel.webview.html = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body></body></html>';
Expand Down Expand Up @@ -849,7 +886,7 @@
}

private buildFindRegex(query: string, options: { regex: boolean; wholeWord: boolean; matchCase: boolean }): RegExp | undefined {
if (!query) return undefined;

Check warning on line 889 in src/CsvEditorProvider.ts

View workflow job for this annotation

GitHub Actions / build

Expected { after 'if' condition
const useRegex = !!options.regex;
const wholeWord = !!options.wholeWord;
const matchCase = !!options.matchCase;
Expand Down Expand Up @@ -941,17 +978,17 @@
}
data[row][col] = '';
} else {
while (data.length <= row) data.push([]);

Check warning on line 981 in src/CsvEditorProvider.ts

View workflow job for this annotation

GitHub Actions / build

Expected { after 'while' condition
while (data[row].length <= col) data[row].push('');

Check warning on line 982 in src/CsvEditorProvider.ts

View workflow job for this annotation

GitHub Actions / build

Expected { after 'while' condition
data[row][col] = value;
}

let trimmed = false;
if (wasEditingLastRow) {
const isRowEmpty = (arr: string[] | undefined) => {
if (!arr || arr.length === 0) return true;

Check warning on line 989 in src/CsvEditorProvider.ts

View workflow job for this annotation

GitHub Actions / build

Expected { after 'if' condition
for (let i = 0; i < arr.length; i++) {
if ((arr[i] ?? '') !== '') return false;

Check warning on line 991 in src/CsvEditorProvider.ts

View workflow job for this annotation

GitHub Actions / build

Expected { after 'if' condition
}
return true;
};
Expand Down Expand Up @@ -989,7 +1026,7 @@
const data = result.data as string[][];
for (const row of data) {
if (index > row.length) {
while (row.length < index) row.push('');

Check warning on line 1029 in src/CsvEditorProvider.ts

View workflow job for this annotation

GitHub Actions / build

Expected { after 'while' condition
}
row.splice(index, 0, '');
}
Expand All @@ -1007,7 +1044,7 @@
}

private async insertColumns(index: number, count: number) {
if (count <= 0) return;

Check warning on line 1047 in src/CsvEditorProvider.ts

View workflow job for this annotation

GitHub Actions / build

Expected { after 'if' condition
this.isUpdatingDocument = true;
const separator = this.getSeparator();
const text = this.document.getText();
Expand All @@ -1016,7 +1053,7 @@
for (let k = 0; k < count; k++) {
for (const row of data) {
if (index > row.length) {
while (row.length < index) row.push('');

Check warning on line 1056 in src/CsvEditorProvider.ts

View workflow job for this annotation

GitHub Actions / build

Expected { after 'while' condition
}
row.splice(index, 0, '');
}
Expand Down Expand Up @@ -1059,7 +1096,7 @@
}

private async deleteColumns(indices: number[]) {
if (!indices || !indices.length) return;

Check warning on line 1099 in src/CsvEditorProvider.ts

View workflow job for this annotation

GitHub Actions / build

Expected { after 'if' condition
this.isUpdatingDocument = true;
const separator = this.getSeparator();
const text = this.document.getText();
Expand Down Expand Up @@ -1690,6 +1727,12 @@
return !matches;
}

private getMediaUri(...segments: string[]): vscode.Uri {
return this.context.extensionUri.with({
path: path.posix.join(this.context.extensionUri.path, 'media', ...segments)
});
}

private wrapHtml(args: {
webview: vscode.Webview;
nonce: string;
Expand All @@ -1707,9 +1750,8 @@
}): string {
const { webview, nonce, fontFamily, fontSize, cellPadding, separator, tableHtml, chunksJson, extraColumnColorCss, nextChunkStart, hasRemoteChunks, mouseWheelZoomEnabled, mouseWheelZoomInvert } = args;
const isDark = vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark;
// Build script URI using file path for compatibility (older APIs may lack Uri.joinPath)
const scriptUri = webview.asWebviewUri(
vscode.Uri.file(path.join(this.context.extensionPath, 'media', 'main.js'))
this.getMediaUri('main.js')
);

// Safe separator transport (assumes single character; see assumptions)
Expand Down Expand Up @@ -2539,17 +2581,12 @@
): Promise<void> {
console.log(`CSV(reg): creating controller for ${document.uri.toString()}`);
const controller = new CsvEditorController(this.context);
// Track active controller
webviewPanel.onDidChangeViewState(e => {
if (e.webviewPanel.active) {
CsvEditorProvider.currentActive = controller;
}
});
await controller.resolveCustomTextEditor(document, webviewPanel, _token);
}

public static getActiveProvider(): CsvEditorController | undefined {
return CsvEditorProvider.currentActive || CsvEditorProvider.editors.find(ed => ed.isActive());
const current = CsvEditorProvider.currentActive;
return current?.isActive() ? current : CsvEditorProvider.editors.find(ed => ed.isActive());
}

public static getHiddenRowsForUri(context: vscode.ExtensionContext, uri: vscode.Uri): number {
Expand Down
35 changes: 31 additions & 4 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,38 @@ async function toggleBooleanConfig(key: string, defaultVal: boolean, messagePref
CsvEditorProvider.editors.forEach(ed => ed.refresh());
}

export function registerCsvCommands(context: vscode.ExtensionContext) {
async function toggleCsvEnabled(synchronizeRouting: () => Promise<void>): Promise<void> {
const config = vscode.workspace.getConfiguration('csv');
const inspected = config.inspect<boolean>('enabled');
const target = inspected?.workspaceValue !== undefined ? vscode.ConfigurationTarget.Workspace : vscode.ConfigurationTarget.Global;
const previous = target === vscode.ConfigurationTarget.Workspace ? inspected?.workspaceValue : inspected?.globalValue;
const next = !config.get<boolean>('enabled', true);
await config.update('enabled', next, target);
try {
await synchronizeRouting();
} catch (error) {
// Do not report Off while VS Code still routes searches into the custom view.
const current = config.inspect<boolean>('enabled');
const value = target === vscode.ConfigurationTarget.Workspace ? current?.workspaceValue : current?.globalValue;
if (value === next) {
await config.update('enabled', previous, target);
try { await synchronizeRouting(); } catch (restoreError) { console.error('CSV: routing rollback failed', restoreError); }
}
console.error('CSV: extension toggle was not applied', error);
return;
}
vscode.window.showInformationMessage(`CSV extension ${next ? 'enabled' : 'disabled'}.`);
CsvEditorProvider.editors.slice().forEach(ed => ed.refresh());
}

export function registerCsvCommands(context: vscode.ExtensionContext, synchronizeRouting: () => Promise<void> = async () => {}) {
let pendingToggle: Promise<void> = Promise.resolve();
context.subscriptions.push(
vscode.commands.registerCommand('csv.toggleExtension', () =>
toggleBooleanConfig('enabled', true, 'CSV extension')
),
vscode.commands.registerCommand('csv.toggleExtension', () => {
const next = pendingToggle.catch(() => {}).then(() => toggleCsvEnabled(synchronizeRouting));
pendingToggle = next;
return next;
}),
vscode.commands.registerCommand('csv.toggleClickableLinks', () =>
toggleBooleanConfig('clickableLinks', true, 'CSV clickable links')
),
Expand Down
Loading
Loading