Colour files by project in the Explorer, Open Editors and tabs

A fair question about the custom list: what does it buy over the built-in
Open Editors view? Most of the answer was the project colours — and those do
not need a custom view. A FileDecorationProvider puts them on the built-in
Open Editors view, the Explorer and the tabs, which keeps everything native
that view already does, including the drag-to-dock this list cannot offer.

verticalTabs.decorateFiles is off / color / colorAndBadge, defaulting to
color. Badges are the two characters a FileDecoration allows, taken from the
first and last segments of the project name so that Acme.Shop.Client and
Acme.Shop.Server read as AC and AS rather than colliding.

Two limits are documented rather than papered over: there is no way to
decorate only the Open Editors view, so the Explorer is coloured too; and Git
decorates modified files with only one colour winning, so a dirty file can
show Git's colour instead of its project's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
max
2026-09-07 18:52:42 +02:00
co-authored by Claude Opus 5
parent 925e55d619
commit 2865ced2f0
5 changed files with 153 additions and 0 deletions
+35
View File
@@ -3,6 +3,7 @@ import * as path from 'path';
import * as vscode from 'vscode';
import { closeOthers, closeTab, togglePin } from '../../actions';
import { ProjectDecorations, badgeFor } from '../../decorations';
import { projectColorIndex, projectOf } from '../../projects';
import {
TabEntry, buildEntries, describeEntry, duplicateLabels, sortEntries, tabUri,
@@ -84,6 +85,40 @@ suite('Project resolution', () => {
});
});
suite('File decorations', () => {
test('a badge keeps sibling projects apart', () => {
assert.strictEqual(badgeFor('Acme.Shop.Client'), 'AC');
assert.strictEqual(badgeFor('Acme.Shop.Server'), 'AS');
assert.strictEqual(badgeFor('Nerfed.Runtime'), 'NR');
assert.strictEqual(badgeFor('Nerfed.Editor'), 'NE');
// A single-word project has no last segment to borrow from.
assert.strictEqual(badgeFor('MoonWorks'), 'Mo');
assert.strictEqual(badgeFor('a'), 'A', 'badges are uppercased');
assert.strictEqual(badgeFor(''), '?');
});
test('decorations name the project and pick its palette colour', async () => {
const decorations = new ProjectDecorations();
decorations.setMode('colorAndBadge');
const inProject = uriFor(FILES[0]);
const decoration = await decorations.provideFileDecoration(inProject);
assert.ok(decoration, 'a file inside a project should be decorated');
assert.strictEqual(decoration.badge, 'NR');
assert.strictEqual(decoration.tooltip, 'Project: Nerfed.Runtime');
assert.deepStrictEqual(decoration.color,
new vscode.ThemeColor(`verticalTabs.project${projectColorIndex('Nerfed.Runtime')}`));
// Colouring parents would repaint most of the Explorer.
assert.strictEqual(decoration.propagate, false);
// Nothing outside a project, and nothing when switched off.
assert.strictEqual(await decorations.provideFileDecoration(uriFor('README.md')), undefined);
decorations.setMode('off');
assert.strictEqual(await decorations.provideFileDecoration(inProject), undefined);
decorations.dispose();
});
});
suite('Tab list', () => {
suiteSetup(async () => {
const ext = vscode.extensions.getExtension('local.vertical-tabs');