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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Working with CSV files shouldn’t be a chore. With CSV, you get:
- **Advanced Multi-Cell Selection:** Easily select and copy blocks of data, then paste them elsewhere as properly formatted CSV.
- **Add/Delete Columns:** Right-click any cell to add a column left or right, or remove the selected column.
- **Add/Delete Rows:** Insert above/below or remove the selected row via context menu.
- **Untitled CSV/TSV:** Choose CSV or TSV language mode in a new untitled text document to open its table without saving first. Separator, header, serial-index, and hidden-row commands work in this view; save the document before changing file encoding. Switching from the untitled table to text commits its live cell, reveals the same native document, and closes the temporary table. Switching back creates a table against that document. Unlike saved-file view replacement, opening an untitled table retains its native source tab because it owns the unsaved document. Use the table’s **Save Untitled Document As…** action or **Ctrl/Cmd+S** in either selection or cell-edit mode. This commits the active cell, reveals the source, and uses VS Code’s normal Save As flow; the temporary table closes when the untitled document becomes a saved file. VS Code’s native File > Save and Save command cannot target a standalone table panel; switch to the source before using those native actions. Changing CSV/TSV language mode preserves pending edits before rebuilding the table.
- **Edit Empty CSVs:** Create or open an empty CSV file and start typing immediately.
- **Column Sorting:** Right-click a header and choose A–Z or Z–A.
- **Custom Font Controls:** Choose a font family and optional font-size override, or inherit VS Code defaults.
Expand Down
11 changes: 6 additions & 5 deletions media/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -1499,6 +1499,12 @@ document.addEventListener('keydown', e => {
return;
}
const key = typeof e.key === 'string' ? e.key.toLowerCase() : '';
if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && key === 's') {
e.preventDefault();
if (editingCell) { editingCell.blur(); }
vscode.postMessage({ type: 'save' });
return;
}
if ((e.ctrlKey || e.metaKey) && key === 'f') {
e.preventDefault();
openFindReplace(false);
Expand Down Expand Up @@ -1760,11 +1766,6 @@ document.addEventListener('keydown', e => {
return;
}

if (editingCell && ((e.ctrlKey || e.metaKey) && e.key === 's')) {
e.preventDefault();
editingCell.blur();
vscode.postMessage({ type: 'save' });
}
if (editingCell && e.key === 'Enter') {
e.preventDefault();
if (e.shiftKey) {
Expand Down
22 changes: 20 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"onLanguage:tsv",
"onCommand:csv.toggleExtension",
"onCommand:csv.toggleEditor",
"onCommand:csv.saveUntitled",
"onCommand:csv.toggleHeader",
"onCommand:csv.toggleSerialIndex",
"onCommand:csv.changeSeparator",
Expand Down Expand Up @@ -71,6 +72,12 @@
"title": "CSV: Switch Between Table and Text",
"icon": "$(arrow-swap)"
},
{
"command": "csv.saveUntitled",
"title": "CSV: Save Untitled Document As\u2026",
"icon": "$(save)",
"enablement": "activeWebviewPanelId == csv.untitledEditor"
},
{
"command": "csv.toggleExtension",
"title": "CSV: Toggle Extension On/Off"
Expand Down Expand Up @@ -112,8 +119,19 @@
"editor/title": [
{
"command": "csv.toggleEditor",
"when": "!isInDiffEditor && (activeCustomEditorId == csv.editor || (activeEditor == workbench.editors.files.textFileEditor && resourceScheme != untitled && resourceExtname =~ /\\.(csv|tsv|tab|psv)$/i))",
"when": "!isInDiffEditor && (activeCustomEditorId == csv.editor || activeWebviewPanelId == csv.untitledEditor || (activeEditor == workbench.editors.files.textFileEditor && resourceScheme != untitled && resourceExtname =~ /\\.(csv|tsv|tab|psv)$/i) || (activeEditor == workbench.editors.textResourceEditor && resourceScheme == untitled && editorLangId =~ /^(csv|tsv)$/))",
"group": "navigation"
},
{
"command": "csv.saveUntitled",
"when": "activeWebviewPanelId == csv.untitledEditor",
"group": "navigation@1"
}
],
"commandPalette": [
{
"command": "csv.saveUntitled",
"when": "activeWebviewPanelId == csv.untitledEditor"
}
]
},
Expand All @@ -129,7 +147,7 @@
"csv.fontFamily": {
"type": "string",
"default": "",
"description": "Font family used by the CSV custom editor. Leave empty to inherit ‘editor.fontFamily’.",
"description": "Font family used by the CSV custom editor. Leave empty to inherit \u2018editor.fontFamily\u2019.",
"scope": "application"
},
"csv.fontSize": {
Expand Down
119 changes: 119 additions & 0 deletions scripts/untitled-editor-smoke.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Run in an isolated VS Code development host using --extensionTestsPath.
const assert = require('assert').strict;
const fs = require('fs');
const os = require('os');
const path = require('path');
const vscode = require('vscode');
const { CsvEditorProvider } = require('../out/CsvEditorProvider');
const { getActiveUntitledCsvEditor } = require('../out/untitled-editors');

async function waitUntil(predicate, message) {
const deadline = Date.now() + 5000;
while (!predicate()) {
if (Date.now() > deadline) { throw new Error(message); }
await new Promise(resolve => setTimeout(resolve, 20));
}
}
async function prepare(document, language, content) {
await vscode.window.showTextDocument(document, { preview: false });
const edit = new vscode.WorkspaceEdit();
edit.insert(document.uri, new vscode.Position(0, 0), content);
assert.equal(await vscode.workspace.applyEdit(edit), true);
const live = await vscode.languages.setTextDocumentLanguage(document, language);
await waitUntil(() => getActiveUntitledCsvEditor()?.document === live, 'language mode should open the untitled table');
assert.equal(live.getText(), content);
assert.ok(vscode.window.tabGroups.all.flatMap(group => group.tabs).some(tab => tab.input.uri?.toString() === live.uri.toString() && tab.isDirty), 'native source must retain its unsaved state');
return live;
}

exports.run = async function () {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'csv-untitled-'));
await vscode.extensions.getExtension('ReprEng.csv').activate();
try {
for (const language of ['csv', 'tsv']) {
const separator = language === 'tsv' ? '\t' : ',';
const filename = path.join(directory, `saved.${language}`);
const uri = vscode.Uri.file(filename).with({ scheme: 'untitled' });
const document = await vscode.workspace.openTextDocument(uri);
await vscode.languages.setTextDocumentLanguage(document, 'plaintext');
const contents = `name${separator}value\nnew${separator}42`;
let live = await prepare(document, language, contents);
await new Promise(resolve => setTimeout(resolve, 200));
await vscode.workspace.getConfiguration('csv').update('enabled', false, vscode.ConfigurationTarget.Global);
await waitUntil(() => vscode.window.activeTextEditor?.document.uri.toString() === live.uri.toString() && !getActiveUntitledCsvEditor(), 'disabling should reveal the native untitled owner');
assert.equal(live.getText(), contents);
assert.equal(fs.existsSync(filename), false, 'disabling must not save');
await vscode.workspace.getConfiguration('csv').update('enabled', true, vscode.ConfigurationTarget.Global);
await waitUntil(() => getActiveUntitledCsvEditor()?.document === live, 'enabling should safely reopen associated-path untitled buffers');
assert.equal(live.getText(), contents);
let controller = CsvEditorProvider.getActiveProvider();
assert.ok(controller);
assert.equal(controller.getCurrentSeparator(), separator);
const oppositeLanguage = language === 'csv' ? 'tsv' : 'csv';
live = await vscode.languages.setTextDocumentLanguage(live, oppositeLanguage);
await waitUntil(() => getActiveUntitledCsvEditor()?.document === live && controller.getCurrentSeparator() === (oppositeLanguage === 'tsv' ? '\t' : ','), 'native language rebind should use its logical extension');
assert.equal(live.getText(), contents, 'language change must not convert source data');
live = await vscode.languages.setTextDocumentLanguage(live, language);
await waitUntil(() => getActiveUntitledCsvEditor()?.document === live && controller.getCurrentSeparator() === separator, 'native language roundtrip should restore its logical extension');
assert.equal(live.getText(), contents);
await controller.updateDocument(1, 1, '43');
const expected = `name${separator}value\nnew${separator}43`;
assert.equal(live.getText(), expected);
const previousPanel = getActiveUntitledCsvEditor().panel;
await vscode.commands.executeCommand('csv.toggleEditor');
assert.equal(vscode.window.activeTextEditor.document.uri.toString(), live.uri.toString());
assert.equal(vscode.window.activeTextEditor.document.getText(), expected);
assert.equal(getActiveUntitledCsvEditor(), undefined, 'source switch closes only the standalone table');
assert.equal(live.isClosed, false, 'native unsaved owner remains live');
assert.equal(fs.existsSync(filename), false, 'source switching must not save');
await new Promise(resolve => setTimeout(resolve, 50));
assert.ok(vscode.window.activeTextEditor, 'explicit source switch must stay in source');
await vscode.commands.executeCommand('csv.toggleEditor');
await waitUntil(() => getActiveUntitledCsvEditor()?.document === live, 'switch should recreate the untitled grid against the same owner');
assert.notEqual(getActiveUntitledCsvEditor().panel, previousPanel);
controller = CsvEditorProvider.getActiveProvider();
const savedExpected = `name${separator}value\nnew${separator}"43\nqueued"`;
void controller.enqueueOperation(() => controller.updateDocument(1, 1, '43\nqueued'));
await vscode.commands.executeCommand('csv.saveUntitled');
await waitUntil(() => live.isClosed && fs.existsSync(filename) && fs.readFileSync(filename, 'utf8') === savedExpected && !CsvEditorProvider.editors.some(candidate => candidate.getDocumentUri().toString() === live.uri.toString()), 'registered native save should finish writing exact bytes and close the temporary grid');
assert.equal(fs.readFileSync(filename, 'utf8'), savedExpected);
await vscode.commands.executeCommand('vscode.openWith', vscode.Uri.file(filename), 'csv.editor', { preview: false });
assert.equal(CsvEditorProvider.getActiveProvider().getCurrentSeparator(), separator);
}

const document = await vscode.workspace.openTextDocument({ language: 'plaintext' });
const live = await prepare(document, 'csv', 'name,value\ncancel,42');
const panel = getActiveUntitledCsvEditor().panel;
const controller = CsvEditorProvider.getActiveProvider();
// Exercise the cancellation result without opening an interactive native picker.
// Associated-path documents above exercise actual native saving and disk bytes.
const executeCommand = vscode.commands.executeCommand;
let cancelledSaveCalls = 0;
vscode.commands.executeCommand = (command, ...args) => {
if (command === 'workbench.action.files.save') {
cancelledSaveCalls += 1;
return Promise.resolve();
}
return executeCommand(command, ...args);
};
try {
await vscode.commands.executeCommand('csv.saveUntitled');
await waitUntil(() => vscode.window.activeTextEditor?.document.uri.toString() === live.uri.toString() && !panel.active, 'registered Save As command should reveal the native owner and finish its view-state transition');
await controller.enqueueOperation(() => {});
} finally {
vscode.commands.executeCommand = executeCommand;
}
assert.equal(cancelledSaveCalls, 1);
assert.equal(live.isClosed, false);
assert.ok(vscode.window.tabGroups.all.flatMap(group => group.tabs).some(tab => tab.input.uri?.toString() === live.uri.toString() && tab.isDirty), 'cancel must retain the dirty native source');
assert.equal(live.getText(), 'name,value\ncancel,42');
await vscode.commands.executeCommand('csv.toggleEditor');
await waitUntil(() => getActiveUntitledCsvEditor()?.panel === panel, 'cancelled Save As must retain and reuse the grid');
panel.dispose();
assert.equal(live.getText(), 'name,value\ncancel,42');
assert.equal(CsvEditorProvider.currentActive, undefined);
console.log('CSV_UNTITLED_SMOKE_OK: pretyped CSV+TSV, language roundtrips, shared edits, source switching, disable/re-enable, registered Save As exact native saves, controlled Save As cancellation retained');
} finally {
fs.rmSync(directory, { recursive: true, force: true });
}
};
Loading
Loading