Draw the tab list as a webview, VS-style

A tree view row cannot be styled: no border, no spacing, and a long file
name gets middle-ellipsized in a narrow strip — which on a real project left
"ClientDogSurger...nionBehaviour.cs" and hid the project text entirely.
Drawing the rows means controlling all of it.

- A coloured left border per project instead of a tinted icon, so the file
  icon slot is free and the name gets the width. The border uses
  var(--vscode-verticalTabs-projectN): VS Code injects every contributed
  theme colour into a webview, so overrides in colorCustomizations still
  apply and nothing is hardcoded.
- The name is split into stem and extension, and only the stem ellipsizes,
  so ".cs" survives on a long name in a narrow view.
- Configurable row spacing and border width, a rule under the pinned block,
  a dirty dot that the close button replaces on hover, italics for preview
  and for tabs that cannot be focused.
- Selection, arrow/Home/End/Enter/Delete keys, middle-click to close, and an
  HTML context menu, since contributes.menus does not reach webview rows.
- Clicking a webview row posts nothing rather than asking for an open that
  would silently do nothing.

Actions moved to actions.ts so the wiring, the view and the tests share one
implementation; extension.ts is now just registration. The per-row commands
are gone — rows act through webview messages — and the palette keeps
close / closeOthers / togglePin acting on the active editor.

Verified in a browser harness at side-bar width: all twelve project colours
resolve, rows stay a consistent 24px, and click, pin, close, middle-click,
keyboard and both context-menu variants post the right intents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
max
2026-09-07 18:34:26 +02:00
co-authored by Claude Opus 5
parent 2e9a43f78b
commit cf168b2e75
8 changed files with 981 additions and 425 deletions
+88
View File
@@ -0,0 +1,88 @@
import * as vscode from 'vscode';
import { canActivate, tabUri } from './tabs';
/**
* What the list can do to a tab.
*
* Everything here takes a live `vscode.Tab`. The rows the view draws are snapshots, and
* acting on stale state silently does the wrong thing (or nothing at all) — an earlier
* version compared a snapshot's `isPinned` and so never unpinned anything.
*/
/**
* Brings a tab to the front by re-opening its resource in its own group. There is no API
* to activate a tab directly, which is also why webview tabs cannot be focused: they
* have no resource to re-open.
*/
export async function openTab(tab: vscode.Tab): Promise<void> {
const uri = tabUri(tab);
if (!uri) {
return;
}
const column = tab.group.viewColumn ?? vscode.ViewColumn.Active;
const input = tab.input;
if (input instanceof vscode.TabInputNotebook) {
const notebook = await vscode.workspace.openNotebookDocument(uri);
await vscode.window.showNotebookDocument(notebook, { viewColumn: column, preview: false });
return;
}
if (input instanceof vscode.TabInputTextDiff) {
await vscode.commands.executeCommand('vscode.diff', input.original, input.modified,
tab.label, { viewColumn: column, preview: false });
return;
}
if (input instanceof vscode.TabInputCustom) {
await vscode.commands.executeCommand('vscode.openWith', uri, input.viewType,
{ viewColumn: column, preview: false });
return;
}
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc, { viewColumn: column, preview: false });
}
export async function closeTab(tab: vscode.Tab): Promise<void> {
await vscode.window.tabGroups.close(tab, true);
}
/**
* VS Code can only pin the *active* editor, so pinning another tab means focusing it
* first. The focus lands where the user clicked anyway, which is what they expect.
*/
export async function togglePin(tab: vscode.Tab): Promise<void> {
const pinned = !tab.isPinned;
if (!tab.isActive) {
if (!canActivate(tab)) {
void vscode.window.showInformationMessage(
`Cannot pin “${tab.label}”: this kind of tab has to be focused first.`);
return;
}
await openTab(tab);
}
await vscode.commands.executeCommand(
pinned ? 'workbench.action.pinEditor' : 'workbench.action.unpinEditor');
}
/** Closes everything except `tab`, keeping pinned tabs the way a tab bar does. */
export async function closeOthers(tab: vscode.Tab): Promise<void> {
const others = vscode.window.tabGroups.all
.flatMap(group => group.tabs)
.filter(other => other !== tab && !other.isPinned);
if (others.length > 0) {
await vscode.window.tabGroups.close(others, true);
}
}
export async function copyPath(tab: vscode.Tab): Promise<void> {
const uri = tabUri(tab);
if (uri) {
await vscode.env.clipboard.writeText(uri.fsPath);
}
}
export async function revealInExplorer(tab: vscode.Tab): Promise<void> {
const uri = tabUri(tab);
if (uri) {
await vscode.commands.executeCommand('revealInExplorer', uri);
}
}