Skip to content
Open
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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Working with CSV files shouldn’t be a chore. With CSV, you get:
- **Column Sorting:** Right-click a header and choose A–Z or Z–A.
- **Custom Font Selection:** Choose a font from a dropdown or inherit VS Code's default.
- **Find & Highlight:** Built-in find widget helps you search for text within your CSV with real-time highlighting and navigation through matches.
- **Clickable Links:** URLs in cells are automatically detected and displayed as clickable links. Ctrl/Cmd+click to open them in your browser.
- **Preserved CSV Integrity:** All modifications respect CSV formatting—no unwanted extra characters or formatting issues.
- **Optimized for Performance:** Designed for medium-sized datasets, ensuring a smooth editing experience without compromising on functionality.
- **Large File Support:** Loads big CSVs in chunks so even large datasets open quickly.
Expand Down Expand Up @@ -85,6 +86,7 @@ Open the Command Palette and search for:
- `CSV: Change Font Family` (`csv.changeFontFamily`)
- `CSV: Hide First N Rows` (`csv.changeIgnoreRows`)
- `CSV: Change File Encoding` (`csv.changeEncoding`)
- `CSV: Toggle Clickable Links` (`csv.toggleClickableLinks`)


## Settings
Expand All @@ -94,7 +96,8 @@ Global (Settings UI or `settings.json`):
- `csv.enabled` (boolean, default `true`): Enable/disable the custom editor.
- `csv.fontFamily` (string, default empty): Override font family; falls back to `editor.fontFamily`.
- `csv.cellPadding` (number, default `4`): Vertical cell padding in pixels.
- Per-file encoding: use `CSV: Change File Encoding` to set a file’s encoding (e.g., `utf8`, `utf16le`, `windows1250`, `gbk`). The extension will reopen the file using the chosen encoding.
- `csv.clickableLinks` (boolean, default `true`): Make URLs in cells clickable. Ctrl/Cmd+click to open links.
- Per-file encoding: use `CSV: Change File Encoding` to set a file's encoding (e.g., `utf8`, `utf16le`, `windows1250`, `gbk`). The extension will reopen the file using the chosen encoding.

Per-file (stored by the extension; set via commands):

Expand Down
44 changes: 42 additions & 2 deletions media/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,25 @@ const showContextMenu = (x, y, row, col) => {
contextMenu.style.display = 'block';
};

document.addEventListener('click', () => { contextMenu.style.display = 'none'; });
document.addEventListener('click', (e) => {
contextMenu.style.display = 'none';

// Handle clicks on CSV links
if (e.target.classList.contains('csv-link')) {
e.preventDefault();
e.stopPropagation();

// Ctrl/Cmd+click to open link
if (e.ctrlKey || e.metaKey) {
const url = e.target.getAttribute('href');
if (url) {
vscode.postMessage({ type: 'openLink', url: url });
}
}
// Regular click just selects the cell (don't start editing)
return;
}
});

/* ──────── UPDATED contextmenu listener ──────── */
table.addEventListener('contextmenu', e => {
Expand All @@ -300,6 +318,10 @@ table.addEventListener('contextmenu', e => {
});

table.addEventListener('mousedown', e => {
// Don't interfere with link clicks
if (e.target.classList.contains('csv-link')) {
return;
}
if(e.target.tagName !== 'TD' && e.target.tagName !== 'TH') return;
const target = e.target;

Expand Down Expand Up @@ -886,7 +908,25 @@ const editCell = (cell, event, mode = 'detail') => {
event ? setCursorAtPoint(cell, event.clientX, event.clientY) : setCursorToEnd(cell);
};

table.addEventListener('dblclick', e => { const target = e.target; if(target.tagName !== 'TD' && target.tagName !== 'TH') return; clearSelection(); editCell(target, e); });
table.addEventListener('dblclick', e => {
const target = e.target;
// Don't enter edit mode when double-clicking a link
if (target.classList.contains('csv-link')) {
e.preventDefault();
e.stopPropagation();
// Ctrl/Cmd+double-click opens the link
if (e.ctrlKey || e.metaKey) {
const url = target.getAttribute('href');
if (url) {
vscode.postMessage({ type: 'openLink', url: url });
}
}
return;
}
if(target.tagName !== 'TD' && target.tagName !== 'TH') return;
clearSelection();
editCell(target, e);
});

const copySelectionToClipboard = () => {
if (currentSelection.length === 0) return;
Expand Down
11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"onCommand:csv.changeSeparator",
"onCommand:csv.changeFontFamily",
"onCommand:csv.changeIgnoreRows",
"onCommand:csv.changeEncoding"
"onCommand:csv.changeEncoding",
"onCommand:csv.toggleClickableLinks"
],
"main": "./out/extension.js",
"contributes": {
Expand All @@ -48,7 +49,8 @@
{ "command": "csv.changeSeparator", "title": "CSV: Change CSV Separator" },
{ "command": "csv.changeFontFamily", "title": "CSV: Change Font Family" },
{ "command": "csv.changeIgnoreRows", "title": "CSV: Hide First N Rows" },
{ "command": "csv.changeEncoding", "title": "CSV: Change File Encoding" }
{ "command": "csv.changeEncoding", "title": "CSV: Change File Encoding" },
{ "command": "csv.toggleClickableLinks", "title": "CSV: Toggle Clickable Links" }
],
"configuration": {
"type": "object",
Expand All @@ -69,6 +71,11 @@
"type": "number",
"default": 4,
"description": "Vertical padding in pixels for table cells."
},
"csv.clickableLinks": {
"type": "boolean",
"default": true,
"description": "Make URLs in cells clickable. Ctrl/Cmd+click to open links."
}
}
},
Expand Down
37 changes: 31 additions & 6 deletions src/CsvEditorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ class CsvEditorController {
case 'sortColumn':
await this.sortColumn(e.index, e.ascending);
break;
case 'openLink':
if (e.url) {
vscode.env.openExternal(vscode.Uri.parse(e.url));
}
break;
}
});

Expand Down Expand Up @@ -562,9 +567,10 @@ class CsvEditorController {
const cellPadding = config.get<number>('cellPadding', 4);
const data = this.trimTrailingEmptyRows((parsed.data || []) as string[][]);
const treatHeader = this.getEffectiveHeader(data, hiddenRows);
const clickableLinks = config.get<boolean>('clickableLinks', true);

const { tableHtml, chunksJson, colorCss } =
this.generateTableAndChunks(data, treatHeader, addSerialIndex, hiddenRows);
this.generateTableAndChunks(data, treatHeader, addSerialIndex, hiddenRows, clickableLinks);

const nonce = this.getNonce();

Expand All @@ -584,7 +590,8 @@ class CsvEditorController {
data: string[][],
treatHeader: boolean,
addSerialIndex: boolean,
hiddenRows: number
hiddenRows: number,
clickableLinks: boolean
): { tableHtml: string; chunksJson: string; colorCss: string } {
let headerFlag = treatHeader;
const totalRows = data.length;
Expand Down Expand Up @@ -625,7 +632,7 @@ class CsvEditorController {
const displayIdx = i + localR + 1; // numbering relative to first visible data row
let cells = '';
for (let cIdx = 0; cIdx < numColumns; cIdx++) {
const safe = this.escapeHtml(row[cIdx] || '');
const safe = this.formatCellContent(row[cIdx] || '', clickableLinks);
cells += `<td tabindex="0" style="min-width:${Math.min(columnWidths[cIdx]||0,100)}ch;max-width:100ch;border:1px solid ${isDark?'#555':'#ccc'};color:${columnColors[cIdx]};overflow:hidden;white-space:nowrap;text-overflow:ellipsis;" data-row="${absRow}" data-col="${cIdx}">${safe}</td>`;
}

Expand Down Expand Up @@ -661,7 +668,7 @@ class CsvEditorController {
: ''
}`;
for (let i = 0; i < numColumns; i++) {
const safe = this.escapeHtml(headerRow[i] || '');
const safe = this.formatCellContent(headerRow[i] || '', clickableLinks);
tableHtml += `<th tabindex="0" style="min-width: ${Math.min(columnWidths[i] || 0, 100)}ch; max-width: 100ch; border: 1px solid ${isDark ? '#555' : '#ccc'}; background-color: ${isDark ? '#1e1e1e' : '#ffffff'}; color: ${columnColors[i]}; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;" data-row="${offset}" data-col="${i}">${safe}</th>`;
}
tableHtml += `</tr></thead><tbody>`;
Expand All @@ -672,7 +679,7 @@ class CsvEditorController {
: ''
}`;
for (let i = 0; i < numColumns; i++) {
const safe = this.escapeHtml(row[i] || '');
const safe = this.formatCellContent(row[i] || '', clickableLinks);
tableHtml += `<td tabindex="0" style="min-width: ${Math.min(columnWidths[i] || 0, 100)}ch; max-width: 100ch; border: 1px solid ${isDark ? '#555' : '#ccc'}; color: ${columnColors[i]}; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;" data-row="${offset + 1 + r}" data-col="${i}">${safe}</td>`;
}
tableHtml += `</tr>`;
Expand All @@ -695,7 +702,7 @@ class CsvEditorController {
: ''
}`;
for (let i = 0; i < numColumns; i++) {
const safe = this.escapeHtml(row[i] || '');
const safe = this.formatCellContent(row[i] || '', clickableLinks);
tableHtml += `<td tabindex="0" style="min-width: ${Math.min(columnWidths[i] || 0, 100)}ch; max-width: 100ch; border: 1px solid ${isDark ? '#555' : '#ccc'}; color: ${columnColors[i]}; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;" data-row="${offset + r}" data-col="${i}">${safe}</td>`;
}
tableHtml += `</tr>`;
Expand Down Expand Up @@ -794,6 +801,8 @@ class CsvEditorController {
td.editing, th.editing { overflow: visible !important; white-space: normal !important; max-width: none !important; }
.highlight { background-color: ${isDark ? '#222222' : '#fefefe'} !important; }
.active-match { background-color: ${isDark ? '#444444' : '#ffffcc'} !important; }
.csv-link { color: ${isDark ? '#6cb6ff' : '#0066cc'}; text-decoration: underline; cursor: pointer; }
.csv-link:hover { color: ${isDark ? '#8ecfff' : '#0044aa'}; }
#findWidget {
position: fixed;
top: 20px;
Expand Down Expand Up @@ -888,6 +897,22 @@ class CsvEditorController {
})[m] as string);
}

private linkifyUrls(escapedText: string): string {
// Match URLs in already-escaped text (handles &amp; in query strings)
// Supports http, https, ftp, mailto protocols
const urlPattern = /(?:https?:\/\/|ftp:\/\/|mailto:)[^\s<>&"']+(?:&amp;[^\s<>&"']+)*/gi;
return escapedText.replace(urlPattern, (url) => {
// Decode &amp; back to & for the href attribute
const href = url.replace(/&amp;/g, '&');
return `<a href="${href}" class="csv-link" title="Ctrl/Cmd+click to open">${url}</a>`;
});
}

private formatCellContent(text: string, linkify: boolean): string {
const escaped = this.escapeHtml(text);
return linkify ? this.linkifyUrls(escaped) : escaped;
}

private escapeCss(text: string): string {
// conservative; ok for font-family lists
return text.replace(/[\\"]/g, m => (m === '\\' ? '\\\\' : '\\"'));
Expand Down
3 changes: 3 additions & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export function registerCsvCommands(context: vscode.ExtensionContext) {
vscode.commands.registerCommand('csv.toggleExtension', () =>
toggleBooleanConfig('enabled', true, 'CSV extension')
),
vscode.commands.registerCommand('csv.toggleClickableLinks', () =>
toggleBooleanConfig('clickableLinks', true, 'CSV clickable links')
),
vscode.commands.registerCommand('csv.toggleHeader', async () => {
const active = CsvEditorProvider.getActiveProvider();
if (!active) { vscode.window.showInformationMessage('Open a CSV/TSV file in the CSV editor.'); return; }
Expand Down
2 changes: 1 addition & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export function activate(context: vscode.ExtensionContext) {
}
}

const keys = ['csv.fontFamily', 'csv.cellPadding'];
const keys = ['csv.fontFamily', 'csv.cellPadding', 'csv.clickableLinks'];
const changed = keys.filter(k => e.affectsConfiguration(k));
if (changed.length) {
CsvEditorProvider.editors.forEach(ed => ed.refresh());
Expand Down