feat(editor): locate the current file from the tab context menu

This commit is contained in:
lxk955 authored and GitHub committed 2026-10-28 00:18:45 +08:00
1 parent f3a3424738
commit 56cc4049e3
8 files changed
+127 -4

No files matched your search

+22 -1
View File
@@ -494,6 +494,15 @@ const isAiPanelMaximized = ref(false);
const isHistoryPanelMaximized = ref(false);
const isZenMode = ref(false);
const showSqlLibraryPanel = ref(safeLocalStorageGet("dbx-sql-library-open") === "true");
const sqlLibraryLocateRequest = ref<{ id: number; fileId: string } | null>(null);
let sqlLibraryLocateRequestId = 0;
watch(
showSqlLibraryPanel,
(open) => {
if (!open) sqlLibraryLocateRequest.value = null;
},
{ flush: "sync" },
);
const sqlFileLocateRequest = ref<{ id: number; path: string } | null>(null);
let sqlFileLocateRequestId = 0;
const showSqlFilePanel = ref(safeLocalStorageGet("dbx-sql-file-panel-open") === "true");
@@ -3740,6 +3749,17 @@ function finishSqlFileLocate(requestId: number) {
if (sqlFileLocateRequest.value?.id === requestId) sqlFileLocateRequest.value = null;
}
function locateSqlLibraryFile(tab: QueryTab) {
if (!tab.savedSqlId || isDetachedWindowContext) return;
isZenMode.value = false;
openRightSidebarPanel("sqlLibrary");
sqlLibraryLocateRequest.value = { id: ++sqlLibraryLocateRequestId, fileId: tab.savedSqlId };
}
function finishSqlLibraryLocate(requestId: number) {
if (sqlLibraryLocateRequest.value?.id === requestId) sqlLibraryLocateRequest.value = null;
}
async function locateTabInSidebar(tab: QueryTab) {
setSidebarOpen(true);
await nextTick();
@@ -5586,6 +5606,7 @@ onUnmounted(() => {
:content-suppressed="activeTab?.mode === 'plugin-workbench'"
@locate-tab="locateTabInSidebar"
@locate-sql-file="locateSqlFile"
@locate-sql-library-file="locateSqlLibraryFile"
@close-tab="
(tabId: string) => {
if (tabId === queryStore.activeTabId) void closeActiveSurface();
@@ -5819,7 +5840,7 @@ onUnmounted(() => {
<div class="panel-resize-handle panel-resize-handle--left" @pointerdown="startSqlLibraryResize" />
<div class="h-full min-h-0 overflow-hidden rounded-[inherit]" @mousedown="rememberAuxiliarySearchSurface('sqlLibrary')">
<div data-sql-library-panel class="h-full min-h-0">
<SqlLibraryPanel @close="closeRightSidebarPanel('sqlLibrary')" />
<SqlLibraryPanel :locate-request="sqlLibraryLocateRequest" @locate-handled="finishSqlLibraryLocate" @close="closeRightSidebarPanel('sqlLibrary')" />
</div>
</div>
</div>
@@ -192,6 +192,7 @@ const groupExecutableSql = computed(() => {
@activate-tab="$emit('activate-tab', $event)"
@locate-tab="$emit('locate-tab', $event)"
@locate-sql-file="$emit('locate-sql-file', $event)"
@locate-sql-library-file="$emit('locate-sql-library-file', $event)"
@toggle-zen-mode="$emit('toggle-zen-mode')"
@start-resize="$emit('start-resize', $event)"
@toggle-collapse="$emit('toggle-collapse')"
@@ -145,6 +145,7 @@ const emit = defineEmits<{
"activate-tab": [tabId: string];
"locate-tab": [tab: QueryTab];
"locate-sql-file": [tab: QueryTab];
"locate-sql-library-file": [tab: QueryTab];
"toggle-zen-mode": [];
"start-resize": [event: PointerEvent];
"toggle-collapse": [];
@@ -1705,6 +1706,12 @@ function getTabMenuItems(tab: QueryTab): ContextMenuItem[] {
visible: !!queryTabExternalFilePath(tab),
action: () => emit("locate-sql-file", tab),
},
{
label: t("sqlFileTree.locateActiveFile"),
icon: Crosshair,
visible: !!tab.savedSqlId,
action: () => emit("locate-sql-library-file", tab),
},
{ label: "", separator: true },
createPinTabMenuItem({
label: tab.pinned ? t("contextMenu.unpinTab") : t("contextMenu.pinTab"),
@@ -78,8 +78,13 @@ const queryStore = useQueryStore();
const settingsStore = useSettingsStore();
const { databaseOptions, loadingDatabaseOptions, loadDatabaseOptions, catalogOptions, loadingCatalogOptions, loadCatalogOptions, catalogDatabaseOptions, loadingCatalogDatabaseOptions, loadCatalogDatabaseOptions } = useDatabaseOptions();
const props = defineProps<{
locateRequest?: { id: number; fileId: string } | null;
}>();
const emit = defineEmits<{
close: [];
"locate-handled": [id: number];
}>();
const UNFILED_DROP_TARGET_ID = "__sql-library-unfiled__";
@@ -541,8 +546,8 @@ const activeSavedSqlId = computed(() => queryStore.tabs.find((tab) => tab.id ===
const libraryTree = ref<HTMLElement | null>(null);
const currentSavedSqlFile = computed(() => (activeSavedSqlId.value ? savedSqlStore.getFile(activeSavedSqlId.value) : undefined));
async function locateCurrentFile() {
const file = currentSavedSqlFile.value;
async function locateFile(fileId: string) {
const file = savedSqlStore.getFile(fileId);
if (!file) return;
if (searchInputPending.value || !treeRows.value.some((row) => row.type === "file" && row.file.id === file.id)) {
searchInput.value?.clear();
@@ -565,6 +570,25 @@ async function locateCurrentFile() {
row?.scrollIntoView({ block: "center", inline: "nearest" });
}
async function locateCurrentFile() {
const file = currentSavedSqlFile.value;
if (!file) return;
await locateFile(file.id);
}
watch(
[() => savedSqlStore.loadState, () => props.locateRequest],
async ([loadState, request]) => {
if (!request || loadState === "loading") return;
try {
await locateFile(request.fileId);
} finally {
emit("locate-handled", request.id);
}
},
{ immediate: true, flush: "post" },
);
const hasCurrentSavedSqlExecutionTarget = computed(() => {
const activeTab = queryStore.tabs.find((tab) => tab.id === queryStore.activeTabId);
const target = savedSqlExecutionTargetFromTab(activeTab);
@@ -1611,6 +1635,12 @@ function showDropAfter(targetId: string) {
function showDropInside(targetId: string) {
return dragState.active && dragState.targetId === targetId && dragState.dropPosition === "inside";
}
defineExpose({
locateFile,
locateCurrentFile,
searchInput,
});
</script>
<template>
@@ -126,7 +126,8 @@ function mountBar(
fullTabTitle: "Full title",
compactTabTitle: "Compact title",
},
sidebar: { locateActiveTab: "Locate" },
sidebar: { locateActiveTab: "Locate", locateSqlFile: "Locate in SQL file list" },
sqlFileTree: { locateActiveFile: "Locate current file" },
},
},
}),
@@ -831,6 +832,37 @@ describe("EditorGroupTabBar behavior", () => {
app.unmount();
host.remove();
});
it("shows locate current file menu item for saved sql tab and emits locate-sql-library-file", async () => {
const store = useQueryStore();
const queryTabId = store.createTab("pg-1", "app", "Query 1", "query");
const savedSqlTabId = store.createTab("pg-1", "app", "Saved Query", "query");
store.tabs.find((tab) => tab.id === savedSqlTabId)!.savedSqlId = "saved-sql-1";
const mainGroup = store.groups[0];
const onLocateSqlLibraryFile = vi.fn();
const { app, host } = mountBar(mainGroup.id, [queryTabId, savedSqlTabId], queryTabId, pinia, undefined, undefined, undefined, undefined, { "onLocate-sql-library-file": onLocateSqlLibraryFile });
await settle();
const rawMenuFor = (tabId: string) => {
const pill = tabPill(host, tabId);
const stub = pill.closest<HTMLElement>(".ctx-menu-stub") as any;
return (stub?.__ctxMenuItems ?? []) as Array<{ label: string; action?: () => void; visible?: boolean }>;
};
const regularLocateItem = rawMenuFor(queryTabId).find((item) => item.label === "Locate current file");
expect(regularLocateItem?.visible).toBe(false);
const savedSqlLocateItem = rawMenuFor(savedSqlTabId).find((item) => item.label === "Locate current file");
expect(savedSqlLocateItem?.visible).toBe(true);
savedSqlLocateItem?.action?.();
expect(onLocateSqlLibraryFile).toHaveBeenCalledTimes(1);
expect(onLocateSqlLibraryFile).toHaveBeenCalledWith(expect.objectContaining({ id: savedSqlTabId, savedSqlId: "saved-sql-1" }));
app.unmount();
host.remove();
});
});
describe("active tab reveal scrolling (issue #12010)", () => {
@@ -174,4 +174,34 @@ describe("SQL library locate and search preferences", () => {
await nextTick();
expect(button.disabled).toBe(true);
});
it("handles locateRequest prop, uncollapses ancestors, scrolls into view and emits locate-handled", async () => {
const onLocateHandled = vi.fn();
const pinia = createPinia();
setActivePinia(pinia);
const store = useSavedSqlStore();
store.loadState = "loaded";
store.folders = [
{ id: "root", name: "Root", connectionId: "", createdAt: timestamp, updatedAt: timestamp },
{ id: "child", parentFolderId: "root", name: "Child", connectionId: "", createdAt: timestamp, updatedAt: timestamp },
];
store.files = [file("unrelated", "unrelated.sql", "SELECT 1"), { ...file("nested-file", "nested.sql", "SELECT 42"), folderId: "child" }];
vi.spyOn(store, "ensureAllFilesLoaded").mockResolvedValue(undefined);
const scroll = vi.spyOn(HTMLElement.prototype, "scrollIntoView").mockImplementation(() => {});
const host = document.createElement("div");
document.body.appendChild(host);
const app = createApp(SqlLibraryPanel, {
locateRequest: { id: 42, fileId: "nested-file" },
"onLocate-handled": onLocateHandled,
});
app.use(pinia);
app.use(i18n);
app.mount(host);
mounted.push({ app, host });
await vi.waitFor(() => expect(onLocateHandled).toHaveBeenCalledWith(42));
expect(visibleFiles(host)).toContain("nested-file");
expect(scroll.mock.instances.at(-1)).toBe(host.querySelector('[data-sql-library-file="nested-file"]'));
});
});
@@ -85,6 +85,7 @@ export interface ContentAreaSurfaceEmits {
closeTab: [tabId: string];
"locate-tab": [tab: QueryTab];
"locate-sql-file": [tab: QueryTab];
"locate-sql-library-file": [tab: QueryTab];
"update:activeOutputView": [tabId: string, value: TabOutputView];
fixWithAi: [tabId: string, errorMessage: string];
sendSelectionToAi: [tabId: string, sql: string];
@@ -10,6 +10,7 @@ export const contentSurfaceEventNames = [
"closeTab",
"locate-tab",
"locate-sql-file",
"locate-sql-library-file",
"update:activeOutputView",
"fixWithAi",
"sendSelectionToAi",