Fix the pin toggle, stale selection, and project detection in Unity repos
Three problems visible on a real project: - The inline pin button showed twice on every unpinned row: the `when` clauses matched /pinned/ and /unpinned/, and "unpinned" contains "pinned". Context values now use unambiguous `pin:on` / `pin:off` tokens, and both commands call one togglePin — the pair exists only so the button's icon can reflect the current state. - The list selected the tab you just left. followActiveTab read `isActive` from the entry snapshot, and after a refresh the tree has not necessarily asked for children yet, so it matched against the previous build. It now rebuilds the list and compares against the live active tab. - Every file in a Unity repo resolved to an arbitrary project. Unity generates one .csproj per assembly into the repo root, and the walk took whichever readDirectory happened to return first. Matches are now sorted, a project file named after its directory wins, and .asmdef — which lives in the assembly's own directory and is what actually defines a Unity assembly — is a default marker, so it is found before reaching the root. Palette widened from 8 to 12 hues, since a hash-assigned colour collides more often than it looks like it should. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+49
-12
@@ -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.",
|
||||
|
||||
+23
-10
@@ -92,12 +92,14 @@ class TabsProvider implements vscode.TreeDataProvider<TabEntry> {
|
||||
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<void> {
|
||||
* 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<void> {
|
||||
if (tab.isPinned === pinned) {
|
||||
return;
|
||||
}
|
||||
async function togglePin(tab: vscode.Tab): Promise<void> {
|
||||
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<void> {
|
||||
}
|
||||
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)!);
|
||||
|
||||
+18
-9
@@ -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<string[]>('projectFiles', ['.csproj']);
|
||||
.get<string[]>('projectFiles', ['.asmdef', '.csproj']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,14 +46,23 @@ export function projectOf(uri: vscode.Uri): Promise<string | undefined> {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user