Skip to content

Add Vertical Tabs compatibility#212

Open
Stonks3141 wants to merge 3 commits intogfxholo:mainfrom
Stonks3141:vertical-tabs
Open

Add Vertical Tabs compatibility#212
Stonks3141 wants to merge 3 commits intogfxholo:mainfrom
Stonks3141:vertical-tabs

Conversation

@Stonks3141
Copy link

Makes icons correctly appear in Vertical Tabs (#204) and fixes the bug where context menus in Vertical Tabs instantly vanish (#110). The Vertical Tabs context menu doesn't have the Change Icon/Remove Icon entries, I couldn't figure out how to make that work.

@Stonks3141 Stonks3141 marked this pull request as draft February 10, 2026 04:20
@Stonks3141
Copy link
Author

Converting to draft, this breaks the change icon menu entry on the file explorer and regular tabs.

@Stonks3141
Copy link
Author

Couldn't figure out a good way to fix the context menu issue, but the icon compatibility is all right. Reverted the broken context menu fix. Should be ready to squash merge.

@Stonks3141 Stonks3141 marked this pull request as ready for review February 12, 2026 00:26
@gfxholo gfxholo added the compatibility Conflicts with another plugin or theme label Feb 12, 2026
@gfxholo gfxholo self-assigned this Feb 12, 2026
@oxdc
Copy link

oxdc commented Feb 15, 2026

Hi @Stonks3141 @gfxholo !

Thanks so much for your work on making Iconic compatible with Vertical Tabs. I’m the developer of Vertical Tabs, and I wanted to let you know that Vertical Tabs now provides an API for modifying menus and setting tab icons. You can find the documentation here. At the moment, the API is available to Beta Program subscribers. If you’d like free access to the beta versions, feel free to reply here or reach out to me directly, and I’ll send you an invitation link.

Thanks again for your contribution!

Comment on lines 3 to +331
@@ -34,6 +41,7 @@ export default class TabIconManager extends IconManager {
});

this.refreshIcons();
this.observeVerticalTabs();
}

/**
@@ -142,6 +150,8 @@ export default class TabIconManager extends IconManager {
}
}
}

this.refreshVerticalTabsIcons(unloading);
}

/**
@@ -238,6 +248,87 @@ export default class TabIconManager extends IconManager {
}
}

/**
* Get the Vertical Tabs plugin container element, if present.
*/
private getVerticalTabsContainer(): HTMLElement | null {
const leaves = this.app.workspace.getLeavesOfType(VERTICAL_TABS_VIEW_TYPE);
if (leaves.length === 0) return null;
return leaves[0].view.containerEl ?? null;
}

/**
* Set up MutationObserver to catch Vertical Tabs React re-renders.
*/
private observeVerticalTabs(): void {
const vtContainer = this.getVerticalTabsContainer();
if (!vtContainer) return;

this.setMutationObserver(vtContainer, { childList: true, subtree: true }, () => {
if (this.vtObserverTimeout) window.cancelAnimationFrame(this.vtObserverTimeout);
this.vtObserverTimeout = window.requestAnimationFrame(() => {
this.refreshVerticalTabsIcons();
});
});
}

/**
* Refresh icons in Vertical Tabs plugin view.
*/
private refreshVerticalTabsIcons(unloading?: boolean): void {
const vtContainer = this.getVerticalTabsContainer();
if (!vtContainer) return;

// Build a map of leaf IDs to leaves
const leafMap = new Map<string, WorkspaceLeaf>();
this.app.workspace.iterateAllLeaves(leaf => {
// @ts-expect-error (Private API)
leafMap.set(leaf.id, leaf);
});

const vtTabs = vtContainer.querySelectorAll('.tree-item.is-tab');

for (const vtTabEl of vtTabs) {
const leafId = vtTabEl.getAttribute('data-id');
if (!leafId) continue;

const vtIconEl = vtTabEl.querySelector(':scope > .tree-item-self > .tree-item-icon') as HTMLElement | null;
if (!vtIconEl) continue;

const leaf = leafMap.get(leafId);
if (!leaf) continue;

const viewType = leaf.view.getViewType();
if (viewType === 'webviewer') continue;
const filePath = leaf.view.getState().file;

let icon: string | null = null;
let color: string | null = null;
let iconDefault: string | null = leaf.view.getIcon();
let category: Category = 'tab';
let id: string = viewType;

if (filePath && !PLUGIN_TAB_TYPES.includes(viewType)) {
category = 'file';
id = typeof filePath === 'string' ? filePath : '';
const fileIcon = this.plugin.settings.fileIcons[id] ?? {};
icon = unloading ? null : fileIcon.icon ?? null;
color = unloading ? null : fileIcon.color ?? null;
} else {
const tabIcon = this.plugin.settings.tabIcons[viewType] ?? {};
icon = unloading ? null : tabIcon.icon ?? null;
color = unloading ? null : tabIcon.color ?? null;
}

const item = { id, name: leaf.getDisplayText(), icon, color, iconDefault, category };
const rule = category === 'file'
? this.plugin.ruleManager.checkRuling('file', id, unloading) ?? item
: item;

this.refreshIcon(rule, vtIconEl);
}
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be faster to use app.workspace.getLeafById(id) than building a map.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

compatibility Conflicts with another plugin or theme

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants