Add Table Editing to a Web Editor: Build a VS Code Extension in TypeScript
vscodeextensioneditor

Add Table Editing to a Web Editor: Build a VS Code Extension in TypeScript

UUnknown
2026-03-04
12 min read
Advertisement

Hands-on 2026 tutorial: build a TypeScript VS Code extension for Markdown/HTML table editing with insert, merge, and CSV import.

Hook: Why VS Code needs a better table UX (and how you can build it)

If you've ever wrestled with Markdown tables, copying CSV into README files, or building editor tooling that needs reliable table editing — you know the pain. VS Code's rich ecosystem helps, but there isn't a single, polished extension that gives full table editing parity with desktop editors (like the tables Microsoft added to Notepad in 2025). In this hands-on guide (2026-ready), you're going to build a TypeScript-based VS Code extension that adds insert, merge, and CSV import table editing for Markdown and HTML blocks inside the editor — complete with an interactive webview grid editor for a great UX.

What you'll build (quick list)

  • Command to insert a Markdown table at the cursor
  • CSV import that converts files into Markdown or HTML tables
  • Interactive Webview editor that edits a table grid and pushes changes back
  • Merge-cells example that converts a Markdown table to an HTML table with colspan/rowspan

Why this matters in 2026

VS Code's editor API and Webview infrastructure matured through late 2025 and early 2026: webviews are faster, content security patterns are stricter, and build tooling favoring esbuild for extensions became mainstream for iteration speed. Meanwhile, teams increasingly expect fast CSV<->table workflows and reliable conversions when maintaining documentation, data files, or export flows. This tutorial targets those real-world workflows and demonstrates best practices for UX and safety in 2026-era VS Code extensions.

Prerequisites

  • Node.js 18+ (or LTS as of 2026)
  • VS Code (latest stable, 2026 edition recommended)
  • Basic TypeScript knowledge
  • Familiarity with VS Code extension basics — if not, install yo code and scaffold a TypeScript extension, but this guide includes complete example files

Project scaffold (fast)

Use the Yeoman generator or a minimal manual scaffold. Below is a concise package.json and tsconfig.json to get started with TypeScript and esbuild-based development:

package.json (key parts)

{
  "name": "vscode-table-editor",
  "version": "0.1.0",
  "main": "dist/extension.js",
  "scripts": {
    "build": "esbuild src/extension.ts --bundle --platform=node --target=node18 --outfile=dist/extension.js",
    "watch": "esbuild src/extension.ts --bundle --platform=node --target=node18 --outfile=dist/extension.js --watch",
    "vscode:prepublish": "npm run build"
  },
  "devDependencies": {
    "esbuild": "^0.18.0",
    "typescript": "^5.3.0",
    "@types/vscode": "^1.83.0"
  },
  "dependencies": {}
}

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "exclude": ["node_modules", ".vscode-test"]
}

Extension manifest (package.json contributions)

Make sure to add commands and activation events so commands show in the command palette and the extension activates when needed:

{
  "contributes": {
    "commands": [
      { "command": "vscode-table.insertTable", "title": "Insert Table" },
      { "command": "vscode-table.importCsv", "title": "Import CSV as Table" },
      { "command": "vscode-table.openTableEditor", "title": "Open Table Editor" },
      { "command": "vscode-table.mergeCells", "title": "Merge Table Cells" }
    ]
  },
  "activationEvents": [
    "onCommand:vscode-table.insertTable",
    "onCommand:vscode-table.importCsv",
    "onCommand:vscode-table.openTableEditor",
    "onCommand:vscode-table.mergeCells"
  ]
}

Core: TypeScript extension code (src/extension.ts)

Below is a focused, runnable example that wires up the core features. This file uses the VS Code API, creates a webview for editing a table, handles CSV import, and inserts Markdown/HTML tables.

import * as vscode from 'vscode';

// Simple CSV parser (handles quoted fields)
function parseCsv(text: string): string[][] {
  const rows: string[][] = [];
  const lines = text.split(/\r?\n/).filter(Boolean);
  for (const line of lines) {
    const row: string[] = [];
    let cur = '';
    let inQuotes = false;
    for (let i = 0; i < line.length; i++) {
      const ch = line[i];
      if (ch === '"') {
        if (inQuotes && line[i + 1] === '"') { cur += '"'; i++; } else { inQuotes = !inQuotes; }
      } else if (ch === ',' && !inQuotes) {
        row.push(cur); cur = '';
      } else { cur += ch; }
    }
    row.push(cur);
    rows.push(row);
  }
  return rows;
}

function markdownFromGrid(grid: string[][]) {
  if (grid.length === 0) return '';
  const colCount = Math.max(...grid.map(r => r.length));
  const header = grid[0].concat(Array(colCount - grid[0].length).fill('')).map(c => ` ${c} `).join('|');
  const sep = Array(colCount).fill(' --- ').join('|');
  const body = grid.slice(1).map(r => r.concat(Array(colCount - r.length).fill('')).map(c => ` ${c} `).join('|')).join('\n');
  return `|${header}|\n|${sep}|\n${body ? '|' + body.split('\n').join('|\n|') : ''}`;
}

function htmlFromGrid(grid: string[][]) {
  return `\n${grid.map(r => '' + r.map(c => ``).join('') + '').join('\n')}\n
${escapeHtml(c)}
`; } function escapeHtml(s: string) { return s.replace(/&/g, '&').replace(//g, '>'); } export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('vscode-table.insertTable', async () => { const editor = vscode.window.activeTextEditor; if (!editor) { vscode.window.showInformationMessage('Open a text editor to insert a table.'); return; } const answer = await vscode.window.showInputBox({ prompt: 'Rows,Cols (e.g. 3,4)', value: '3,3' }); if (!answer) return; const [rStr, cStr] = answer.split(',').map(s => s.trim()); const rows = Math.max(1, Number(rStr) || 3); const cols = Math.max(1, Number(cStr) || 3); const grid = Array.from({ length: rows }, (_, i) => Array.from({ length: cols }, (_, j) => (i === 0 ? `Header ${j + 1}` : `Cell ${i},${j + 1}`))); const md = markdownFromGrid(grid); await editor.edit(edit => { edit.insert(editor.selection.active, md + '\n'); }); }), vscode.commands.registerCommand('vscode-table.importCsv', async () => { const files = await vscode.window.showOpenDialog({ canSelectMany: false, filters: { 'CSV': ['csv', 'txt'] } }); if (!files || !files[0]) return; const doc = await vscode.workspace.openTextDocument(files[0]); const grid = parseCsv(doc.getText()); const insertAs = await vscode.window.showQuickPick(['markdown', 'html'], { placeHolder: 'Insert CSV as...' }); const editor = vscode.window.activeTextEditor; if (!editor) { vscode.window.showInformationMessage('Open a text editor to insert the table.'); return; } const text = insertAs === 'html' ? htmlFromGrid(grid) : markdownFromGrid(grid); await editor.edit(edit => { edit.insert(editor.selection.active, text + '\n'); }); }), vscode.commands.registerCommand('vscode-table.openTableEditor', async () => { const editor = vscode.window.activeTextEditor; if (!editor) { vscode.window.showInformationMessage('Open a text editor and select a table region to edit.'); return; } const selection = editor.selection; const selectedText = editor.document.getText(selection); // If selection is empty, try to grab the table around the cursor (simple heuristic) const regionText = selectedText || getBlockAroundCursor(editor.document, selection.active); if (!regionText) { vscode.window.showInformationMessage('No table found at cursor. Select table text or open a document with a Markdown/HTML table.'); return; } const panel = vscode.window.createWebviewPanel('tableEditor', 'Table Editor', vscode.ViewColumn.One, { enableScripts: true }); const grid = tableToGrid(regionText); panel.webview.html = getWebviewContent(panel.webview, context.extensionUri, grid); // Handle messages from the webview panel.webview.onDidReceiveMessage(msg => { if (msg.type === 'apply') { const newText = msg.format === 'html' ? htmlFromGrid(msg.grid) : markdownFromGrid(msg.grid); editor.edit(edit => edit.replace(selection.isEmpty ? new vscode.Range(editor.document.positionAt(0), editor.document.positionAt(0)) : selection, newText)); panel.dispose(); } }); }), vscode.commands.registerCommand('vscode-table.mergeCells', async () => { const editor = vscode.window.activeTextEditor; if (!editor) { return; } const selection = editor.selection; const text = editor.document.getText(selection); if (!text) { vscode.window.showInformationMessage('Select a table cell or an HTML table to merge.'); return; } // Very simple: if selection is a markdown table, convert whole table to HTML and ask user to edit with the webview if (text.trim().startsWith('|')) { const grid = tableToGrid(text); // Convert to HTML and open the webview editor for advanced actions vscode.commands.executeCommand('vscode-table.openTableEditor'); } else if (text.includes(' with colspan=2 const merged = text.replace(/(.*?)<\/td>\s*(.*?)<\/td>/i, '$1 $2'); await editor.edit(edit => edit.replace(selection, merged)); vscode.window.showInformationMessage('Performed a naive merge. Open the Table Editor for precise merges.'); } else { vscode.window.showInformationMessage('Unsupported selection for merge. Try selecting an HTML table or a Markdown table.'); } }) ); } export function deactivate() {} // Helpers --------------------------------------------- function getBlockAroundCursor(doc: vscode.TextDocument, pos: vscode.Position) { // naive: find nearest lines that look like a markdown table or an HTML table block const maxLines = 200; const start = Math.max(0, pos.line - maxLines); const end = Math.min(doc.lineCount - 1, pos.line + maxLines); let text = doc.getText(new vscode.Range(start, 0, end, doc.lineAt(end).text.length)); // Attempt to extract a table block const mdMatch = text.match(/(^\|[\s\S]*?\n)(?=\n|$)/m); if (mdMatch) return mdMatch[0]; const htmlMatch = text.match(//i); if (htmlMatch) return htmlMatch[0]; return ''; } function tableToGrid(text: string): string[][] { const trimmed = text.trim(); if (trimmed.startsWith('/gi; const cellRegex = /]*>([\s\S]*?)<\/t[dh]>/gi; let r: RegExpExecArray | null; while ((r = rowRegex.exec(trimmed))) { const rowHtml = r[0]; const cells: string[] = []; let c: RegExpExecArray | null; while ((c = cellRegex.exec(rowHtml))) { cells.push(stripTags(c[1]).trim()); } rows.push(cells); } return rows; } // Markdown table parser (simple) const lines = trimmed.split(/\r?\n/).map(l => l.trim()).filter(Boolean); if (lines.length === 0) return []; const pipeLines = lines.filter(l => l.startsWith('|')); if (pipeLines.length === 0) return []; const grid = pipeLines.map(line => line.replace(/^\||\|$/g, '').split('|').map(cell => cell.trim())); return grid; } function stripTags(s: string) { return s.replace(/<[^>]*>/g, ''); } function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri, grid: string[][]) { const initial = JSON.stringify(grid); // simple content: editable table using inputs return ` Table Editor
`; }

Notes on the code

  • CSV parsing uses a compact, resilient parser (handles quoted fields). For production use, consider using a battle-tested library like papaparse for robust CSV dialect support.
  • Markdown→HTML conversion is used to enable merging cells: markdown tables don't support colspan/rowspan, so your extension converts to an HTML table when advanced editing (merge) is needed.
  • Webview sandboxing uses a strict Content Security Policy. In 2026, VS Code tightened default CSP rules — keep scripts minimal and avoid remote sources.
  • Undo/redo works automatically via the editor.edit call; avoid direct filesystem writes from the webview.

UX considerations & patterns

  1. Don't block the editor — use Webviews for interactive editing but keep operations that change the document through the extension process so the VS Code undo stack remains consistent.
  2. Offer formats — some teams want Markdown tables; others want HTML (for merges). Provide both insert/import formats.
  3. Large tables — for very large CSVs, use streaming parsing and warn users before inserting huge blocks to avoid editor slowdowns.
  4. Accessibility — label inputs in your webview and consider keyboard-first interactions for power users.
  5. Edge detection — implement robust detection of a table region around the cursor; the example uses heuristics and should be hardened for production.

Testing & debugging

Launch the extension with the VS Code debugger (F5). Use the watch esbuild task during development and keep a dev instance open. For unit and integration tests, use the vscode-test runner and isolate document manipulations — test CSV import, insertion, and webview message handling.

Packaging & publishing (2026 tips)

  • Use esbuild for fast CI builds — it's become the recommended approach for quick iteration and smaller bundles.
  • Sign and publish to the Visual Studio Marketplace, or use Open VSX for wider editor compatibility. Automation: GitHub Actions that run esbuild, package vsix, and publish on tag pushes.
  • Include thorough README usage examples and a changelog. Recent marketplace trends (late 2025–early 2026) show extensions with clear screenshots, short video clips, and one-click install flows convert better.

Advanced strategies & future directions

  • Integrate Language Server or DocumentSemanticTokens to highlight table regions in the editor for better selection UX.
  • Use LLMs for smart CSV parsing and header inference — e.g., guess column types, prettify headers, or generate frontmatter when inserting tables.
  • Support collaborative table editing using Live Share or a backend sync for teams editing documentation together.
  • Provide transformer plugins (e.g., support for AsciiDoc, reStructuredText) so the extension becomes a table gateway for multiple markup formats.

Common pitfalls and how to avoid them

  • Breaking undo/redo: always perform document edits via the VS Code API (editor.edit) so users can undo changes naturally.
  • Security mistakes: never allow arbitrary remote code or inline eval from webview content; prefer message passing and sanitize all input.
  • Performance issues: avoid inserting megabytes directly into an open editor without user confirmation; provide a preview or stream-insert strategy.
  • Markdown limitations: explain to users when you convert to HTML (e.g., after merges) so the document behavior doesn't surprise them.

Actionable checklist before publishing

  1. Polish table detection heuristics and add tests
  2. Replace simple CSV parser with a robust library if you expect complex CSV dialects
  3. Harden Webview CSP and remove unsafe-inline where possible
  4. Add screenshots and quick tutorial in README showing insert, import, and merge workflows
  5. Instrument minimal telemetry (opt-in) to understand how users edit tables

Key takeaways

  • Table editing in VS Code is tractable: combine editor text transforms for simple edits and a webview for complex, grid-like UX.
  • Markdown's limits require HTML when you need advanced features like merged cells — be explicit with users when conversions occur.
  • In 2026, use fast bundlers (esbuild), strict webview CSPs, and consider AI/LLM-assisted CSV parsing for better developer experience.
"Ship a small, reliable core (insert + CSV import). Add interactive editing (webview) and advanced features (merge) iteratively."

Next steps & call-to-action

Try the example code above: scaffold a new extension, drop the TypeScript into src/extension.ts, run npm run build, then F5 to test. If you found this tutorial useful, fork it and extend it with multi-selection merges, LLM header inference, or a dedicated table panel. Share your repo and feedback so we can refine a community-backed table editor for VS Code.

Want a production-ready starter with tests, CI, and pipeline to publish? Visit the companion repo (linked in the article page) for a complete scaffold and prebuilt CI workflow optimized for 2026 tooling. Open issues describing your table workflows — I'm interested in the merge strategies you need for real documents.

Advertisement

Related Topics

#vscode#extension#editor
U

Unknown

Contributor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.

Advertisement
2026-03-04T00:28:53.361Z