feat(tabs): locate tab objects in sidebar

Closes #6681
This commit is contained in:
t8y2 committed 2026-08-22 10:31:17 +00:00
1 parent 53b7be3c6f
commit 906ea8ba26
8 files changed
+104 -16

No files matched your search

@@ -331,30 +331,37 @@ export function shouldScrollActiveSidebarSelection(options: { activeTabId: strin
return options.activeTabId !== options.previousActiveTabId || (options.autoSelectEnabled && options.previousAutoSelectEnabled === false);
}
// nearest is used for passive auto-selection; smart keeps context for explicit locate actions.
export type SidebarNodeScrollAlign = "nearest" | "top" | "smart";
// nearest is used for passive auto-selection; smart keeps context for the existing toolbar action.
export type SidebarNodeScrollAlign = "nearest" | "top" | "smart" | "center";
export function scrollTopForSidebarNode(options: { index: number; currentScrollTop: number; viewportHeight: number; rowHeight?: number; topOcclusionHeight?: number; align?: SidebarNodeScrollAlign }): number {
export function scrollTopForSidebarNode(options: { index: number; currentScrollTop: number; viewportHeight: number; scrollHeight?: number; rowHeight?: number; topOcclusionHeight?: number; align?: SidebarNodeScrollAlign }): number {
const rowHeight = options.rowHeight ?? SIDEBAR_TREE_ROW_HEIGHT;
if (options.index < 0 || options.viewportHeight <= 0) return options.currentScrollTop;
const rowTop = options.index * rowHeight;
const rowBottom = rowTop + rowHeight;
const topOcclusionHeight = options.topOcclusionHeight ?? 0;
if (options.align === "top") return Math.max(0, rowTop - topOcclusionHeight);
const maxScrollTop = options.scrollHeight == null ? Number.POSITIVE_INFINITY : Math.max(0, options.scrollHeight - options.viewportHeight);
const clampScrollTop = (value: number) => Math.max(0, Math.min(maxScrollTop, value));
if (options.align === "top") return clampScrollTop(rowTop - topOcclusionHeight);
if (options.align === "smart") {
// Similar to IDE Locate: place the target around the upper third of the viewport.
const availableViewportHeight = Math.max(rowHeight, options.viewportHeight - topOcclusionHeight);
const smartOffset = Math.max(0, (availableViewportHeight - rowHeight) / 3);
return Math.max(0, Math.round(rowTop - topOcclusionHeight - smartOffset));
return clampScrollTop(Math.round(rowTop - topOcclusionHeight - smartOffset));
}
if (options.align === "center") {
const availableViewportHeight = Math.max(rowHeight, options.viewportHeight - topOcclusionHeight);
const centerOffset = Math.max(0, (availableViewportHeight - rowHeight) / 2);
return clampScrollTop(Math.round(rowTop - topOcclusionHeight - centerOffset));
}
const viewportTop = options.currentScrollTop + topOcclusionHeight;
const viewportBottom = options.currentScrollTop + options.viewportHeight;
if (rowTop < viewportTop) return Math.max(0, rowTop - topOcclusionHeight);
if (rowBottom > viewportBottom) return Math.max(0, rowBottom - options.viewportHeight);
return options.currentScrollTop;
if (rowTop < viewportTop) return clampScrollTop(rowTop - topOcclusionHeight);
if (rowBottom > viewportBottom) return clampScrollTop(rowBottom - options.viewportHeight);
return clampScrollTop(options.currentScrollTop);
}
export function findNodePathForActiveTab(tab: QueryTab | undefined | null, treeNodes: readonly TreeNode[]): TreeNode[] | null {