fix(sidebar): locate active table reliably

This commit is contained in:
auth0 authored and GitHub committed 2026-07-08 20:21:13 +08:00
1 parent 33b4fc1dac
commit 07482557e6
3 files changed
+221 -14

No files matched your search

@@ -15,7 +15,7 @@ import { connectionPasteTargetGroupId, selectedConnectionClipboardNodes, selecte
import { isEditableSidebarTypeSearchTarget, sidebarTypeSearchNextQuery } from "@/lib/sidebar/sidebarTypeSearch";
import { usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport";
import { connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { activeTabSidebarTarget, findSidebarNodeForActiveTab, findSidebarNodeForTarget, findNodePathForTarget, scrollTopForSidebarNode, shouldScrollActiveSidebarSelection, type ActiveTabSidebarTarget } from "@/lib/sidebar/sidebarActiveTabTarget";
import { activeTabSidebarTarget, findSidebarNodeForActiveTab, findSidebarNodeForTarget, findNodePathForTarget, scrollTopForSidebarNode, shouldScrollActiveSidebarSelection, type ActiveTabSidebarTarget, type SidebarNodeScrollAlign } from "@/lib/sidebar/sidebarActiveTabTarget";
import { findLoadedTableTargetForCandidate, queryContextTargetFromCandidate, queryCursorTableCandidate, type QueryCursorTableCandidate } from "@/lib/sql/queryCursorTableTarget";
import { SIDEBAR_TREE_ROW_HEIGHT, SIDEBAR_TREE_PRERENDER_COUNT, SIDEBAR_TREE_SCROLL_BUFFER, flattenTree, shouldVirtualizeFlatTree, type FlatTreeNode } from "@/composables/useFlatTree";
import { sidebarTreeContextKey } from "@/lib/sidebar/sidebarTreeContext";
@@ -534,13 +534,33 @@ const pendingRenameGroupId = ref<string | null>(null);
const highlightedNodeId = ref<string | null>(null);
let highlightTimer: number | undefined;
// 等待虚拟列表渲染后再高亮。
function waitForSidebarRenderFrame(): Promise<void> {
return new Promise((resolve) => {
window.requestAnimationFrame(() => resolve());
});
}
// 重新触发定位高亮,支持连续定位同一节点。
async function flashSidebarNode(nodeId: string) {
window.clearTimeout(highlightTimer);
highlightedNodeId.value = null;
await nextTick();
await waitForSidebarRenderFrame();
highlightedNodeId.value = nodeId;
highlightTimer = window.setTimeout(() => {
if (highlightedNodeId.value === nodeId) highlightedNodeId.value = null;
}, 1800);
}
function topOcclusionHeightForSidebarNode(nodeId: string): number {
const sticky = stickyNode.value;
if (!useVirtualTree.value || !sticky || sticky.id === nodeId) return 0;
return SIDEBAR_TREE_ROW_HEIGHT;
}
async function scrollToSidebarNode(nodeId: string) {
async function scrollToSidebarNode(nodeId: string, options?: { align?: SidebarNodeScrollAlign }) {
await nextTick();
const index = flatNodes.value.findIndex((item) => item.id === nodeId);
@@ -552,6 +572,7 @@ async function scrollToSidebarNode(nodeId: string) {
currentScrollTop: scroller.scrollTop,
viewportHeight: scroller.clientHeight,
topOcclusionHeight: topOcclusionHeightForSidebarNode(nodeId),
...(options?.align ? { align: options.align } : {}),
});
if (nextScrollTop !== scroller.scrollTop) {
scroller.scrollTop = nextScrollTop;
@@ -661,13 +682,8 @@ async function locateActiveTabInSidebar() {
store.treeSelectionAnchorId = match.id;
await nextTick();
window.clearTimeout(highlightTimer);
highlightedNodeId.value = match.id;
highlightTimer = window.setTimeout(() => {
highlightedNodeId.value = null;
}, 1800);
await scrollToSidebarNode(match.id);
await scrollToSidebarNode(match.id, { align: "smart" });
await flashSidebarNode(match.id);
}
function tableTargetFromCandidate(candidate: QueryCursorTableCandidate): ActiveTabSidebarTarget {
@@ -143,7 +143,13 @@ export function activeTabSidebarTarget(tab: QueryTab | undefined | null): Active
function schemaMatches(node: TreeNode, schema: string | undefined): boolean {
if (!schema) return true;
return (node.schema || "") === schema;
if ((node.schema || "") === schema) return true;
// Some database engines (for example MySQL in database-object tree mode) do
// not store a separate schema on table nodes. In that case the database name
// is the effective schema, so a target schema equal to the node database
// should still resolve to the loaded table node.
return !node.schema && (node.database || "") === schema;
}
export function matchesTarget(node: TreeNode, target: ActiveTabSidebarTarget): boolean {
@@ -207,16 +213,28 @@ export function shouldScrollActiveSidebarSelection(options: { activeTabId: strin
return options.activeTabId !== options.previousActiveTabId || (options.autoSelectEnabled && options.previousAutoSelectEnabled === false);
}
export function scrollTopForSidebarNode(options: { index: number; currentScrollTop: number; viewportHeight: number; rowHeight?: number; topOcclusionHeight?: number }): number {
// nearest is used for passive auto-selection; smart keeps context for explicit locate actions.
export type SidebarNodeScrollAlign = "nearest" | "top" | "smart";
export function scrollTopForSidebarNode(options: { index: number; currentScrollTop: number; viewportHeight: 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 viewportTop = options.currentScrollTop + (options.topOcclusionHeight ?? 0);
const topOcclusionHeight = options.topOcclusionHeight ?? 0;
if (options.align === "top") return Math.max(0, 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));
}
const viewportTop = options.currentScrollTop + topOcclusionHeight;
const viewportBottom = options.currentScrollTop + options.viewportHeight;
if (rowTop < viewportTop) return Math.max(0, rowTop - (options.topOcclusionHeight ?? 0));
if (rowTop < viewportTop) return Math.max(0, rowTop - topOcclusionHeight);
if (rowBottom > viewportBottom) return Math.max(0, rowBottom - options.viewportHeight);
return options.currentScrollTop;
}