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:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user