diff --git a/package.json b/package.json index 2eecf57..0ed0a0c 100644 --- a/package.json +++ b/package.json @@ -115,12 +115,12 @@ "view/item/context": [ { "command": "verticalTabs.pin", - "when": "view == verticalTabs.tabs && viewItem =~ /unpinned/", + "when": "view == verticalTabs.tabs && viewItem =~ /pin:off/", "group": "inline@1" }, { "command": "verticalTabs.unpin", - "when": "view == verticalTabs.tabs && viewItem =~ /pinned/", + "when": "view == verticalTabs.tabs && viewItem =~ /pin:on/", "group": "inline@1" }, { @@ -130,12 +130,12 @@ }, { "command": "verticalTabs.pin", - "when": "view == verticalTabs.tabs && viewItem =~ /unpinned/", + "when": "view == verticalTabs.tabs && viewItem =~ /pin:off/", "group": "1_state@1" }, { "command": "verticalTabs.unpin", - "when": "view == verticalTabs.tabs && viewItem =~ /pinned/", + "when": "view == verticalTabs.tabs && viewItem =~ /pin:on/", "group": "1_state@1" }, { @@ -239,6 +239,7 @@ "type": "string" }, "default": [ + ".asmdef", ".csproj", ".fsproj", ".vbproj", @@ -259,7 +260,7 @@ "description": "Project colour 1.", "defaults": { "dark": "#4ec9b0", - "light": "#267f99", + "light": "#227d68", "highContrast": "#4ec9b0" } }, @@ -268,7 +269,7 @@ "description": "Project colour 2.", "defaults": { "dark": "#dcdcaa", - "light": "#795e26", + "light": "#7a6e17", "highContrast": "#dcdcaa" } }, @@ -277,7 +278,7 @@ "description": "Project colour 3.", "defaults": { "dark": "#9cdcfe", - "light": "#001080", + "light": "#0b5f92", "highContrast": "#9cdcfe" } }, @@ -286,7 +287,7 @@ "description": "Project colour 4.", "defaults": { "dark": "#c586c0", - "light": "#af00db", + "light": "#8b338a", "highContrast": "#c586c0" } }, @@ -295,7 +296,7 @@ "description": "Project colour 5.", "defaults": { "dark": "#ce9178", - "light": "#a31515", + "light": "#a3441f", "highContrast": "#ce9178" } }, @@ -304,7 +305,7 @@ "description": "Project colour 6.", "defaults": { "dark": "#b5cea8", - "light": "#098658", + "light": "#3c7a2e", "highContrast": "#b5cea8" } }, @@ -313,7 +314,7 @@ "description": "Project colour 7.", "defaults": { "dark": "#569cd6", - "light": "#0000ff", + "light": "#1d4f91", "highContrast": "#569cd6" } }, @@ -322,10 +323,46 @@ "description": "Project colour 8.", "defaults": { "dark": "#d7ba7d", - "light": "#b8860b", + "light": "#8a6512", "highContrast": "#d7ba7d" } }, + { + "id": "verticalTabs.project9", + "description": "Project colour 9.", + "defaults": { + "dark": "#f28b82", + "light": "#b3261e", + "highContrast": "#f28b82" + } + }, + { + "id": "verticalTabs.project10", + "description": "Project colour 10.", + "defaults": { + "dark": "#a5d6a7", + "light": "#2e7d32", + "highContrast": "#a5d6a7" + } + }, + { + "id": "verticalTabs.project11", + "description": "Project colour 11.", + "defaults": { + "dark": "#b39ddb", + "light": "#5e35b1", + "highContrast": "#b39ddb" + } + }, + { + "id": "verticalTabs.project12", + "description": "Project colour 12.", + "defaults": { + "dark": "#80cbc4", + "light": "#00695c", + "highContrast": "#80cbc4" + } + }, { "id": "verticalTabs.noProject", "description": "Colour for files that belong to no project.", diff --git a/src/extension.ts b/src/extension.ts index 4c3da92..39856f8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -92,12 +92,14 @@ class TabsProvider implements vscode.TreeDataProvider { item.iconPath = new vscode.ThemeIcon('circle-filled'); } + // Tokens have to be unambiguous as substrings: a `when` clause matching + // /pinned/ would also match "unpinned", showing both pin and unpin at once. const state = [ - entry.isPinned ? 'pinned' : 'unpinned', + entry.isPinned ? 'pin:on' : 'pin:off', entry.uri ? 'file' : 'other', entry.canActivate ? 'activatable' : 'inert', ]; - item.contextValue = state.join('.'); + item.contextValue = state.join(' '); const tooltip = new vscode.MarkdownString(); tooltip.appendMarkdown(`**${entry.label}**${entry.isDirty ? ' — unsaved' : ''}\n\n`); @@ -166,10 +168,8 @@ async function openTab(tab: vscode.Tab): Promise { * 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. */ -async function setPinned(tab: vscode.Tab, pinned: boolean): Promise { - if (tab.isPinned === pinned) { - return; - } +async function togglePin(tab: vscode.Tab): Promise { + const pinned = !tab.isPinned; if (!tab.isActive) { if (!canActivate(tab)) { void vscode.window.showInformationMessage( @@ -178,6 +178,8 @@ async function setPinned(tab: vscode.Tab, pinned: boolean): Promise { } await openTab(tab); } + // Both the pin and unpin commands land here: the two exist only so the inline + // button's icon can reflect the current state. await vscode.commands.executeCommand( pinned ? 'workbench.action.pinEditor' : 'workbench.action.unpinEditor'); } @@ -194,12 +196,23 @@ export function activate(context: vscode.ExtensionContext): void { view.description = count > 0 ? String(count) : undefined; }; - /** Keeps the list's selection on whatever tab is active. */ + /** + * Keeps the list's selection on whatever tab is active. + * + * The entries hold a snapshot of `isActive`, and after a refresh the tree has not + * necessarily asked for children yet — so rebuild the list first and match against + * the live active tab rather than a stale flag. + */ const followActiveTab = async () => { if (!view.visible) { return; } - const active = provider.find(entry => entry.isActive); + const activeTab = vscode.window.tabGroups.activeTabGroup.activeTab; + if (!activeTab) { + return; + } + const entries = await provider.getChildren(); + const active = entries.find(entry => entry.tab === activeTab); if (active && !view.selection.some(entry => entry.id === active.id)) { try { await view.reveal(active, { select: true, focus: false }); @@ -271,11 +284,11 @@ export function activate(context: vscode.ExtensionContext): void { }), vscode.commands.registerCommand('verticalTabs.pin', (arg?: unknown) => { const tab = tabOf(arg); - return tab ? setPinned(tab, true) : undefined; + return tab ? togglePin(tab) : undefined; }), vscode.commands.registerCommand('verticalTabs.unpin', (arg?: unknown) => { const tab = tabOf(arg); - return tab ? setPinned(tab, false) : undefined; + return tab ? togglePin(tab) : undefined; }), vscode.commands.registerCommand('verticalTabs.copyPath', async (arg?: unknown) => { const uri = tabOf(arg) && tabUri(tabOf(arg)!); diff --git a/src/projects.ts b/src/projects.ts index 597c91a..970a081 100644 --- a/src/projects.ts +++ b/src/projects.ts @@ -2,11 +2,11 @@ import * as vscode from 'vscode'; import * as path from 'path'; /** How many project colours `contributes.colors` declares. */ -const PALETTE_SIZE = 8; +const PALETTE_SIZE = 12; function markers(): string[] { return vscode.workspace.getConfiguration('verticalTabs') - .get('projectFiles', ['.csproj']); + .get('projectFiles', ['.asmdef', '.csproj']); } /** @@ -46,14 +46,23 @@ export function projectOf(uri: vscode.Uri): Promise { return undefined; } - const match = entries.find(([name, type]) => type === vscode.FileType.File && - (extensions.has(path.extname(name).toLowerCase()) || names.has(name.toLowerCase()))); - if (match) { - const [name] = match; + // A directory can hold several project files — a Unity repo root has one + // generated .csproj per assembly. Sort so the choice is at least stable, and + // prefer one named after its directory, which is the usual convention. + const matches = entries + .filter(([name, type]) => type === vscode.FileType.File && + (extensions.has(path.extname(name).toLowerCase()) || names.has(name.toLowerCase()))) + .map(([name]) => name) + .sort((a, b) => a.localeCompare(b)); + + if (matches.length > 0) { + const dirName = path.basename(current); + const match = matches.find( + name => path.basename(name, path.extname(name)) === dirName) ?? matches[0]; // `Foo.csproj` names itself; `package.json` names its directory. - return extensions.has(path.extname(name).toLowerCase()) - ? path.basename(name, path.extname(name)) - : path.basename(current); + return extensions.has(path.extname(match).toLowerCase()) + ? path.basename(match, path.extname(match)) + : dirName; } if (!root || current === root || path.dirname(current) === current) {