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
+24 -115
View File
@@ -28,69 +28,41 @@
{
"id": "verticalTabs.tabs",
"name": "Tabs",
"type": "webview",
"contextualTitle": "Tabs",
"visibility": "visible"
}
]
},
"viewsWelcome": [
{
"view": "verticalTabs.tabs",
"contents": "No editors are open."
}
],
"commands": [
{
"command": "verticalTabs.open",
"title": "Open Tab",
"category": "Tabs"
},
{
"command": "verticalTabs.close",
"title": "Close",
"category": "Tabs",
"icon": "$(close)"
"title": "Close Active Tab",
"category": "Tabs"
},
{
"command": "verticalTabs.closeOthers",
"title": "Close Others",
"title": "Close Other Tabs",
"category": "Tabs"
},
{
"command": "verticalTabs.pin",
"title": "Pin",
"category": "Tabs",
"icon": "$(pin)"
},
{
"command": "verticalTabs.unpin",
"title": "Unpin",
"category": "Tabs",
"icon": "$(pinned)"
},
{
"command": "verticalTabs.copyPath",
"title": "Copy Path",
"category": "Tabs"
},
{
"command": "verticalTabs.revealInExplorer",
"title": "Reveal in Explorer View",
"command": "verticalTabs.togglePin",
"title": "Pin or Unpin Active Tab",
"category": "Tabs"
},
{
"command": "verticalTabs.setOrderOpen",
"title": "Sort by Open Order",
"title": "Sort Tabs by Open Order",
"category": "Tabs"
},
{
"command": "verticalTabs.setOrderProject",
"title": "Sort by Project",
"title": "Sort Tabs by Project",
"category": "Tabs"
},
{
"command": "verticalTabs.setOrderName",
"title": "Sort by File Name",
"title": "Sort Tabs by File Name",
"category": "Tabs"
}
],
@@ -111,83 +83,6 @@
"when": "view == verticalTabs.tabs",
"group": "1_order@3"
}
],
"view/item/context": [
{
"command": "verticalTabs.pin",
"when": "view == verticalTabs.tabs && viewItem =~ /pin:off/",
"group": "inline@1"
},
{
"command": "verticalTabs.unpin",
"when": "view == verticalTabs.tabs && viewItem =~ /pin:on/",
"group": "inline@1"
},
{
"command": "verticalTabs.close",
"when": "view == verticalTabs.tabs",
"group": "inline@2"
},
{
"command": "verticalTabs.pin",
"when": "view == verticalTabs.tabs && viewItem =~ /pin:off/",
"group": "1_state@1"
},
{
"command": "verticalTabs.unpin",
"when": "view == verticalTabs.tabs && viewItem =~ /pin:on/",
"group": "1_state@1"
},
{
"command": "verticalTabs.close",
"when": "view == verticalTabs.tabs",
"group": "2_close@1"
},
{
"command": "verticalTabs.closeOthers",
"when": "view == verticalTabs.tabs",
"group": "2_close@2"
},
{
"command": "verticalTabs.copyPath",
"when": "view == verticalTabs.tabs && viewItem =~ /file/",
"group": "3_path@1"
},
{
"command": "verticalTabs.revealInExplorer",
"when": "view == verticalTabs.tabs && viewItem =~ /file/",
"group": "3_path@2"
}
],
"commandPalette": [
{
"command": "verticalTabs.open",
"when": "false"
},
{
"command": "verticalTabs.close",
"when": "false"
},
{
"command": "verticalTabs.closeOthers",
"when": "false"
},
{
"command": "verticalTabs.pin",
"when": "false"
},
{
"command": "verticalTabs.unpin",
"when": "false"
},
{
"command": "verticalTabs.copyPath",
"when": "false"
},
{
"command": "verticalTabs.revealInExplorer",
"when": "false"
}
]
},
"configuration": {
@@ -226,7 +121,7 @@
"verticalTabs.colorByProject": {
"type": "boolean",
"default": true,
"description": "Colour each tab's icon by its containing project."
"description": "Colour each row's left border by its containing project."
},
"verticalTabs.allGroups": {
"type": "boolean",
@@ -251,6 +146,20 @@
"build.gradle"
],
"description": "File names or extensions that mark a project root. The nearest match above a file names its project."
},
"verticalTabs.rowSpacing": {
"type": "number",
"default": 2,
"minimum": 0,
"maximum": 10,
"description": "Pixels of vertical space between rows."
},
"verticalTabs.borderWidth": {
"type": "number",
"default": 3,
"minimum": 0,
"maximum": 8,
"description": "Width in pixels of the project colour border on the left of each row."
}
}
},