feat(ui): route Ctrl+F search by focused surface

This commit is contained in:
zipg authored and GitHub committed 2026-09-15 23:20:05 +08:00
1 parent 4c80d16e26
commit 3c8ae159fb
23 files changed
+233 -56

No files matched your search

+75 -8
View File
@@ -174,10 +174,13 @@ type AiAssistantHandle = {
setPrompt: (text: string) => void;
addTableMention: (target: { schema?: string; table: string }) => void;
clearContextReferences: () => void;
focusSearch: () => boolean;
/** Opens a conversation by id (used by the background-run toast, §9). */
selectConversationById: (conversationId: string) => void;
};
type AuxiliarySearchSurface = "ai" | "history" | "sqlLibrary" | null;
const { t } = useI18n();
const connectionStore = useConnectionStore();
const queryStore = useQueryStore();
@@ -365,6 +368,7 @@ const aiAssistantRef = ref<AiAssistantHandle | null>(null);
const appSidebarRef = ref<InstanceType<typeof AppSidebar> | null>(null);
const appTabBarRef = ref<InstanceType<typeof AppTabBar> | null>(null);
const contentAreaRef = ref<InstanceType<typeof SqlEditorWorkspace> | null>(null);
const lastFocusedAuxiliarySurface = ref<AuxiliarySearchSurface>(null);
const selectedSql = ref("");
const cursorPos = ref(0);
@@ -1428,6 +1432,9 @@ function setRightSidebarPanelOpen(panelId: RightSidebarPanelId, open: boolean) {
applyRightSidebarPanelState(transitionRightSidebarPanels(currentRightSidebarPanelState(), panelId, open, exclusive));
if (open) {
lastOpenedRightSidebarPanel = panelId;
if (panelId === "ai" || panelId === "history" || panelId === "sqlLibrary") {
lastFocusedAuxiliarySurface.value = panelId;
}
} else if (lastOpenedRightSidebarPanel === panelId) {
lastOpenedRightSidebarPanel = RIGHT_SIDEBAR_PANEL_IDS.find((candidate) => rightSidebarPanelRefs[candidate].value);
}
@@ -3148,6 +3155,14 @@ function handleTabSwitcherKeydownCapture(e: KeyboardEvent) {
tabSwitcherKeyboard.handleKeydownCapture(e);
}
function handleAuxiliarySearchKeydownCapture(e: KeyboardEvent) {
if (e.defaultPrevented || !isFocusSearchShortcut(e, settingsStore.editorSettings.shortcuts)) return;
const target = e.target instanceof Element ? e.target : document.activeElement instanceof Element ? document.activeElement : null;
if (!focusSearchInAuxiliarySurface(target)) return;
e.preventDefault();
e.stopPropagation();
}
function handleTabSwitcherWindowBlur() {
tabSwitcherKeyboard.handleWindowBlur();
}
@@ -3180,6 +3195,50 @@ function handleNativeSelectAll(e: KeyboardEvent) {
if (shouldBlockAppNativeSelectAll(e)) e.preventDefault();
}
function focusSearchInput(selector: string): boolean {
const input = Array.from(document.querySelectorAll<HTMLInputElement>(selector)).find((candidate) => candidate.getClientRects().length > 0);
if (!input) return false;
input.focus();
input.select();
return true;
}
function focusSearchInAuxiliarySurface(target: Element | null): boolean {
if (showConnectionDialog.value) {
// The dialog is modal: keep the shortcut inside it and never focus a
// surface hidden behind the overlay, even when its search input is absent.
focusSearchInput("[data-connection-db-search]");
return true;
}
if (showSettingsPage.value) return focusSearchInput("[data-settings-global-search]");
if (showDriverStore.value) {
const selector = driverStoreActiveTab.value === "jdbc" ? "[data-driver-store-jdbc-search]" : "[data-driver-store-agent-search]";
return focusSearchInput(selector);
}
if (showPluginCenter.value) return focusSearchInput("[data-plugin-marketplace-search]");
if (showAiPanel.value && target?.closest("[data-ai-assistant-root], [data-ai-conversation-search]")) {
if (aiAssistantRef.value) return aiAssistantRef.value.focusSearch();
invokeWhenAiReady((handle) => handle.focusSearch());
return true;
}
if (showHistory.value && target?.closest("[data-history-panel], [data-history-search]")) return focusSearchInput("[data-history-search]");
if (showSqlLibraryPanel.value && target?.closest("[data-sql-library-panel], [data-sql-library-search]")) return focusSearchInput("[data-sql-library-search]");
if (lastFocusedAuxiliarySurface.value === "ai" && showAiPanel.value) {
if (aiAssistantRef.value) return aiAssistantRef.value.focusSearch();
invokeWhenAiReady((handle) => handle.focusSearch());
return true;
}
if (lastFocusedAuxiliarySurface.value === "history" && showHistory.value) return focusSearchInput("[data-history-search]");
if (lastFocusedAuxiliarySurface.value === "sqlLibrary" && showSqlLibraryPanel.value) return focusSearchInput("[data-sql-library-search]");
return false;
}
function rememberAuxiliarySearchSurface(surface: Exclude<AuxiliarySearchSurface, null>) {
lastFocusedAuxiliarySurface.value = surface;
}
function setPluginWorkbenchTabRef(tabId: string, element: unknown) {
if (element && typeof element === "object" && "refresh" in element && typeof element.refresh === "function") {
pluginWorkbenchTabRefs.set(tabId, element as PluginWorkbenchTabHandle);
@@ -3252,9 +3311,11 @@ async function handleKeydown(e: KeyboardEvent) {
return;
}
if (isFocusSearchShortcut(e, shortcuts)) {
// Route by the event target so focus inside the shared result grid (or a
// Teleported cell-detail dialog) lands on the result search, not the editor.
const focused = contentAreaRef.value?.focusSearch(e.target instanceof Element ? e.target : null) || appSidebarRef.value?.focusSearch();
// Keep the focused navigation surface ahead of the active content tab.
// Otherwise Ctrl+F from empty space in the sidebar incorrectly opens the
// search belonging to the active table/query tab.
const target = e.target instanceof Element ? e.target : null;
const focused = target?.closest("[data-app-sidebar]") ? appSidebarRef.value?.focusSearch(target) : focusSearchInAuxiliarySurface(target) || contentAreaRef.value?.focusSearch(target) || appSidebarRef.value?.focusSearch(target);
if (focused) {
e.preventDefault();
e.stopPropagation();
@@ -3547,6 +3608,7 @@ onMounted(async () => {
void applyUiScale(settingsStore.editorSettings.uiScale);
window.addEventListener("keydown", handleNativeSelectAll, true);
window.addEventListener("keydown", handleTabSwitcherKeydownCapture, true);
window.addEventListener("keydown", handleAuxiliarySearchKeydownCapture, true);
window.addEventListener("keydown", handleKeydown);
window.addEventListener("keyup", handleKeyup, true);
window.addEventListener("blur", handleTabSwitcherWindowBlur);
@@ -3628,6 +3690,7 @@ onUnmounted(() => {
}
window.removeEventListener("keydown", handleNativeSelectAll, true);
window.removeEventListener("keydown", handleTabSwitcherKeydownCapture, true);
window.removeEventListener("keydown", handleAuxiliarySearchKeydownCapture, true);
window.removeEventListener("keydown", handleKeydown);
window.removeEventListener("keyup", handleKeyup, true);
window.removeEventListener("blur", handleTabSwitcherWindowBlur);
@@ -3958,7 +4021,7 @@ onUnmounted(() => {
:style="isAiPanelMaximized ? {} : { width: aiPanelWidth + 'px' }"
>
<div v-if="!isAiPanelMaximized" class="panel-resize-handle panel-resize-handle--left" @pointerdown="startAiPanelResize" />
<div class="h-full min-h-0 overflow-hidden rounded-[inherit]">
<div class="h-full min-h-0 overflow-hidden rounded-[inherit]" @mousedown="rememberAuxiliarySearchSurface('ai')">
<AiAssistant
v-if="aiPanelReady"
ref="aiAssistantRef"
@@ -3985,8 +4048,10 @@ onUnmounted(() => {
:style="{ width: historyWidth + 'px' }"
>
<div class="panel-resize-handle panel-resize-handle--left" @pointerdown="startHistoryResize" />
<div class="h-full min-h-0 overflow-hidden rounded-[inherit]">
<QueryHistory :current-connection-id="activeTab?.connectionId" :current-database="activeTab?.database" @restore="restoreHistorySql" @analyze-ai="analyzeHistoryWithAi" @close="closeRightSidebarPanel('history')" />
<div class="h-full min-h-0 overflow-hidden rounded-[inherit]" @mousedown="rememberAuxiliarySearchSurface('history')">
<div data-history-panel class="h-full min-h-0">
<QueryHistory :current-connection-id="activeTab?.connectionId" :current-database="activeTab?.database" @restore="restoreHistorySql" @analyze-ai="analyzeHistoryWithAi" @close="closeRightSidebarPanel('history')" />
</div>
</div>
</div>
@@ -3997,8 +4062,10 @@ onUnmounted(() => {
:style="{ width: sqlLibraryWidth + 'px' }"
>
<div class="panel-resize-handle panel-resize-handle--left" @pointerdown="startSqlLibraryResize" />
<div class="h-full min-h-0 overflow-hidden rounded-[inherit]">
<SqlLibraryPanel @close="closeRightSidebarPanel('sqlLibrary')" />
<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')" />
</div>
</div>
</div>