feat(shortcuts): add search focus shortcut

This commit is contained in:
t8y2
2026-05-13 17:51:20 +08:00
parent 42501cc8fb
commit e9158390c1
9 changed files with 176 additions and 10 deletions
+13 -1
View File
@@ -34,7 +34,7 @@ import * as api from "@/lib/api";
import { resolveDefaultDatabase } from "@/lib/defaultDatabase";
import { resolveExecutableSql } from "@/lib/sqlExecutionTarget";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import { isCloseTabShortcut, isExecuteSqlShortcut } from "@/lib/keyboardShortcuts";
import { isCloseTabShortcut, isExecuteSqlShortcut, isFocusSearchShortcut } from "@/lib/keyboardShortcuts";
import { isPreviewTab } from "@/lib/tabPresentation";
import { SQL_FILE_UNSUPPORTED_TYPES } from "@/lib/databaseCapabilities";
import { classifyAiSqlExecution } from "@/lib/aiSqlExecutionPolicy";
@@ -80,6 +80,8 @@ const showAiPanel = ref(localStorage.getItem("dbx-ai-panel-open") !== "false");
const { sidebarWidth, aiPanelWidth, historyWidth, startSidebarResize, startAiPanelResize, startHistoryResize } =
usePanelResize();
const aiAssistantRef = ref<InstanceType<typeof AiAssistant> | null>(null);
const appSidebarRef = ref<InstanceType<typeof AppSidebar> | null>(null);
const contentAreaRef = ref<InstanceType<typeof ContentArea> | null>(null);
const selectedSql = ref("");
const cursorPos = ref(0);
@@ -409,6 +411,14 @@ function onAiRequestAutoExecuteSql(sql: string) {
}
function handleKeydown(e: KeyboardEvent) {
if (isFocusSearchShortcut(e)) {
const focused = contentAreaRef.value?.focusSearch() || appSidebarRef.value?.focusSearch();
if (focused) {
e.preventDefault();
e.stopPropagation();
}
return;
}
if (isCloseTabShortcut(e)) {
e.preventDefault();
if (showDriverStore.value) {
@@ -574,6 +584,7 @@ onUnmounted(() => {
"
>
<AppSidebar
ref="appSidebarRef"
:sidebar-width="sidebarWidth"
:classic-layout="isClassicLayout"
@import="dialogs.onImportClick"
@@ -614,6 +625,7 @@ onUnmounted(() => {
@clear-default-database="clearActiveDefaultDatabase"
/>
<ContentArea
ref="contentAreaRef"
:active-tab="activeTab"
:active-connection="activeConnection"
:executable-sql="executableSql"
+14 -2
View File
@@ -72,6 +72,7 @@ import { isHiddenGridColumn, usesSyntheticRowIdKey } from "@/lib/tableEditing";
import { formatGridSqlLiteral } from "@/lib/dataGridSql";
import { matchesRowStatusFilter, type RowStatus, type RowStatusFilter } from "@/lib/gridRowStatus";
import { displayCellValue, type CellValue } from "@/lib/cellValue";
import { isCancelSearchShortcut } from "@/lib/keyboardShortcuts";
import { useToast } from "@/composables/useToast";
import { useDataGridExport } from "@/composables/useDataGridExport";
@@ -460,6 +461,15 @@ function navigateSuggestion(delta: number) {
suggestionIndex.value = Math.min(Math.max(suggestionIndex.value + delta, 0), searchSuggestions.value.length - 1);
}
function focusSearch(): boolean {
const input = searchInputRef.value;
if (!input) return false;
input.focus();
input.select();
updateSuggestionPosition();
return true;
}
const PAIRS: Record<string, string> = { "'": "'", '"': '"', "(": ")" };
function onSearchKeydown(e: KeyboardEvent) {
@@ -503,7 +513,7 @@ function onSearchKeydown(e: KeyboardEvent) {
acceptSuggestion();
return;
}
if (e.key === "Escape") {
if (isCancelSearchShortcut(e)) {
e.preventDefault();
dismissSuggestions();
return;
@@ -519,7 +529,8 @@ function onSearchKeydown(e: KeyboardEvent) {
return;
}
}
if (e.key === "Escape") {
if (isCancelSearchShortcut(e)) {
e.preventDefault();
searchText.value = "";
}
}
@@ -1742,6 +1753,7 @@ defineExpose({
onToolbarRollback,
showDdl,
toggleDdl,
focusSearch,
});
</script>
+9 -1
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { Upload, Download, RefreshCw } from "lucide-vue-next";
import { Button } from "@/components/ui/button";
@@ -27,6 +28,7 @@ const emit = defineEmits<{
const { t } = useI18n();
const connectionStore = useConnectionStore();
const { toast } = useToast();
const connectionTreeRef = ref<InstanceType<typeof ConnectionTree>>();
async function refreshTree() {
try {
@@ -35,6 +37,12 @@ async function refreshTree() {
toast(t("connection.connectFailed", { message: e?.message || String(e) }), 5000);
}
}
function focusSearch(): boolean {
return connectionTreeRef.value?.focusSearch() ?? false;
}
defineExpose({ focusSearch });
</script>
<template>
@@ -86,7 +94,7 @@ async function refreshTree() {
</Tooltip>
</div>
<div class="flex-1 min-h-0">
<ConnectionTree />
<ConnectionTree ref="connectionTreeRef" />
</div>
</div>
<div class="panel-resize-handle panel-resize-handle--right" @mousedown="emit('startResize', $event)" />
+18 -1
View File
@@ -58,6 +58,8 @@ const columnInfoColumns = ref<ColumnInfo[]>([]);
const columnInfoLoading = ref(false);
const columnInfoError = ref<string | undefined>(undefined);
const dataGridRef = ref<InstanceType<typeof DataGrid>>();
const redisKeyBrowserRef = ref<InstanceType<typeof RedisKeyBrowser>>();
const objectBrowserRef = ref<InstanceType<typeof ObjectBrowser>>();
const activeSqlFormatDialect = computed<SqlFormatDialect>(() => {
switch (props.activeConnection?.db_type) {
@@ -160,6 +162,15 @@ function onHandleCloseColumnPanel() {
columnInfoColumns.value = [];
columnInfoError.value = undefined;
}
function focusSearch(): boolean {
if (props.activeTab.mode === "redis") return redisKeyBrowserRef.value?.focusSearch() ?? false;
if (props.activeTab.mode === "objects") return objectBrowserRef.value?.focusSearch() ?? false;
if (props.activeTab.mode === "query" && props.activeOutputView !== "result") return false;
return dataGridRef.value?.focusSearch() ?? false;
}
defineExpose({ focusSearch });
</script>
<template>
@@ -433,7 +444,12 @@ function onHandleCloseColumnPanel() {
<!-- Redis mode: key browser -->
<template v-else-if="activeTab.mode === 'redis'">
<div class="flex-1 min-h-0">
<RedisKeyBrowser :key="activeTab.id" :connection-id="activeTab.connectionId" :db="Number(activeTab.database)" />
<RedisKeyBrowser
ref="redisKeyBrowserRef"
:key="activeTab.id"
:connection-id="activeTab.connectionId"
:db="Number(activeTab.database)"
/>
</div>
</template>
@@ -452,6 +468,7 @@ function onHandleCloseColumnPanel() {
<!-- Objects mode: virtualized database object browser -->
<template v-else-if="activeTab.mode === 'objects' && activeConnection">
<ObjectBrowser
ref="objectBrowserRef"
:key="`${activeTab.id}-${activeTab.objectBrowser?.schema || ''}`"
:connection="activeConnection"
:database="activeTab.database"
+30 -2
View File
@@ -37,6 +37,7 @@ import { buildExecutableObjectSourceSql, objectSourceSaveExecutionMode } from "@
import { useQueryStore } from "@/stores/queryStore";
import QueryEditor from "@/components/editor/QueryEditor.vue";
import type { SqlFormatDialect } from "@/lib/sqlFormatter";
import { isCancelSearchShortcut } from "@/lib/keyboardShortcuts";
type ObjectRow = {
id: string;
@@ -66,6 +67,7 @@ const queryStore = useQueryStore();
const schemas = ref<string[]>([]);
const selectedSchema = ref<string | undefined>(props.schema);
const rows = ref<ObjectRow[]>([]);
const rootRef = ref<HTMLElement>();
const search = ref("");
const objectFilter = ref<ObjectFilter>("all");
const loadingSchemas = ref(false);
@@ -411,6 +413,26 @@ function filterLabel(filter: ObjectFilter) {
return `${t(key)} ${filterCount(filter)}`;
}
function getSearchInput(): HTMLInputElement | null {
return rootRef.value?.querySelector<HTMLInputElement>("[data-object-search-input]") ?? null;
}
function focusSearch(): boolean {
const input = getSearchInput();
if (!input) return false;
input.focus();
input.select();
return true;
}
function onSearchKeydown(event: KeyboardEvent) {
if (!isCancelSearchShortcut(event)) return;
event.preventDefault();
search.value = "";
}
defineExpose({ focusSearch });
watch(
() => [props.connection.id, props.database, props.schema] as const,
() => {
@@ -422,7 +444,7 @@ watch(
</script>
<template>
<div class="flex h-full min-h-0 flex-col bg-background">
<div ref="rootRef" class="flex h-full min-h-0 flex-col bg-background">
<div class="flex h-10 shrink-0 items-center gap-2 border-b px-3">
<div class="flex min-w-0 flex-1 items-center gap-2">
<Table2 class="h-4 w-4 text-muted-foreground" />
@@ -435,7 +457,13 @@ watch(
</div>
<div class="flex min-w-[240px] flex-1 items-center gap-2">
<Search class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<Input v-model="search" class="h-7 text-xs" :placeholder="t('objects.search')" />
<Input
v-model="search"
data-object-search-input
class="h-7 text-xs"
:placeholder="t('objects.search')"
@keydown="onSearchKeydown"
/>
<div v-if="showObjectFilter" class="flex h-7 shrink-0 items-center rounded border bg-muted/20 p-0.5">
<button
v-for="filter in objectFilters"
+30 -2
View File
@@ -33,6 +33,7 @@ import {
type RedisKeyTreeNode,
} from "@/lib/redisKeyTree";
import { classifyRedisCommandSafety } from "@/lib/redisCommandSafety";
import { isCancelSearchShortcut } from "@/lib/keyboardShortcuts";
const { t } = useI18n();
@@ -45,6 +46,7 @@ const flatKeys = ref<RedisKeyInfo[]>([]);
const treeKeys = ref<RedisKeyTreeNode[]>([]);
const loading = ref(false);
const loadingMore = ref(false);
const rootRef = ref<HTMLElement>();
const searchPattern = ref("*");
const selectedKeyRaw = ref<string | null>(null);
const hasMore = ref(false);
@@ -317,27 +319,53 @@ function onSearchInput() {
searchTimer = setTimeout(loadKeys, 400);
}
function getSearchInput(): HTMLInputElement | null {
return rootRef.value?.querySelector<HTMLInputElement>("[data-redis-search-input]") ?? null;
}
function focusSearch(): boolean {
const input = getSearchInput();
if (!input) return false;
input.focus();
input.select();
return true;
}
function onSearchKeydown(event: KeyboardEvent) {
if (event.key === "Enter") {
void loadKeys();
return;
}
if (!isCancelSearchShortcut(event)) return;
event.preventDefault();
searchPattern.value = "*";
void loadKeys();
}
onUnmounted(() => {
if (searchTimer) clearTimeout(searchTimer);
});
onMounted(loadKeys);
defineExpose({ focusSearch });
</script>
<template>
<Splitpanes class="h-full" horizontal>
<!-- Key table (top) -->
<Pane :size="selectedKey ? 50 : 100" :min-size="25">
<div class="h-full flex flex-col overflow-hidden">
<div ref="rootRef" class="h-full flex flex-col overflow-hidden">
<!-- Toolbar -->
<div class="h-9 flex items-center gap-1 px-2 border-b shrink-0">
<Search class="w-3.5 h-3.5 text-muted-foreground shrink-0" />
<Input
v-model="searchPattern"
data-redis-search-input
class="h-6 text-xs border-0 shadow-none focus-visible:ring-0"
:placeholder="t('redis.pattern')"
@input="onSearchInput"
@keydown.enter="loadKeys"
@keydown="onSearchKeydown"
/>
<Button variant="ghost" size="icon" class="h-6 w-6 shrink-0" @click="loadKeys">
<Loader2 v-if="loading" class="h-3 w-3 animate-spin" />
+20
View File
@@ -5,6 +5,7 @@ import { Search, X, ListFilter, Check, FolderPlus } from "lucide-vue-next";
import { useConnectionStore } from "@/stores/connectionStore";
import type { TreeNode } from "@/types/database";
import { matchSidebarLabel } from "@/lib/sidebarSearch";
import { isCancelSearchShortcut } from "@/lib/keyboardShortcuts";
import {
SIDEBAR_TREE_ROW_HEIGHT,
SIDEBAR_TREE_PRERENDER_COUNT,
@@ -30,6 +31,7 @@ const { t } = useI18n();
const store = useConnectionStore();
const searchQuery = ref("");
const deferredSearchQuery = ref("");
const searchInputRef = ref<HTMLInputElement>();
const selectedTypes = ref<string[]>([]);
const searchCollapsedIds = ref<Set<string>>(new Set());
let searchTimer: number | undefined;
@@ -184,6 +186,22 @@ function onSearchToggle(node: TreeNode) {
else next.delete(node.id);
searchCollapsedIds.value = next;
}
function focusSearch(): boolean {
const input = searchInputRef.value;
if (!input) return false;
input.focus();
input.select();
return true;
}
function onSearchKeydown(event: KeyboardEvent) {
if (!isCancelSearchShortcut(event)) return;
event.preventDefault();
searchQuery.value = "";
}
defineExpose({ focusSearch });
</script>
<template>
@@ -193,12 +211,14 @@ function onSearchToggle(node: TreeNode) {
<div class="relative flex-1">
<Search class="absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground" />
<input
ref="searchInputRef"
v-model="searchQuery"
autocapitalize="off"
autocorrect="off"
spellcheck="false"
class="w-full h-6 pl-7 pr-6 text-xs rounded border border-border bg-background focus:outline-none focus:ring-1 focus:ring-ring"
:placeholder="t('grid.search')"
@keydown="onSearchKeydown"
/>
<button
v-if="searchQuery"
+12
View File
@@ -18,3 +18,15 @@ export function isCloseTabShortcut(event: ShortcutLikeEvent): boolean {
if (!event.metaKey) return false;
return event.key.toLowerCase() === "w";
}
export function isFocusSearchShortcut(event: ShortcutLikeEvent): boolean {
if (event.isComposing) return false;
if (event.altKey) return false;
if (!event.metaKey && !event.ctrlKey) return false;
return event.key.toLowerCase() === "f";
}
export function isCancelSearchShortcut(event: ShortcutLikeEvent): boolean {
if (event.isComposing) return false;
return event.key === "Escape";
}