diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 8915c2ad6c..6388e931e0 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -275,8 +275,6 @@ const editActiveCustomThemeId = ref(settingsStore.editorSettings.activeCustomThe const showThemeCustomizer = ref(false); const editExecuteMode = ref(settingsStore.editorSettings.executeMode); const editExecuteAllOnBlankLine = ref(settingsStore.editorSettings.executeAllOnBlankLine); -const editGlobalConnectTimeoutSecs = ref(settingsStore.editorSettings.globalConnectTimeoutSecs); -const editGlobalQueryTimeoutSecs = ref(settingsStore.editorSettings.globalQueryTimeoutSecs); const editShowExecutionTargetPicker = ref(settingsStore.editorSettings.showExecutionTargetPicker); const editShowStatementRunButtons = ref(settingsStore.editorSettings.showStatementRunButtons); const editShowCurrentStatementFrame = ref(settingsStore.editorSettings.showCurrentStatementFrame); @@ -465,8 +463,6 @@ function currentEditorSettingsDraft(): EditorSettingsDraft { activeCustomThemeId: editActiveCustomThemeId.value, executeMode: editExecuteMode.value, executeAllOnBlankLine: editExecuteAllOnBlankLine.value, - globalConnectTimeoutSecs: editGlobalConnectTimeoutSecs.value, - globalQueryTimeoutSecs: editGlobalQueryTimeoutSecs.value, showExecutionTargetPicker: editShowExecutionTargetPicker.value, showStatementRunButtons: editShowStatementRunButtons.value, showCurrentStatementFrame: editShowCurrentStatementFrame.value, @@ -738,8 +734,6 @@ function syncEditorSettingsDraftFromStore() { editActiveCustomThemeId.value = settingsStore.editorSettings.activeCustomThemeId; editExecuteMode.value = settingsStore.editorSettings.executeMode; editExecuteAllOnBlankLine.value = settingsStore.editorSettings.executeAllOnBlankLine; - editGlobalConnectTimeoutSecs.value = settingsStore.editorSettings.globalConnectTimeoutSecs; - editGlobalQueryTimeoutSecs.value = settingsStore.editorSettings.globalQueryTimeoutSecs; editShowExecutionTargetPicker.value = settingsStore.editorSettings.showExecutionTargetPicker; editShowStatementRunButtons.value = settingsStore.editorSettings.showStatementRunButtons; editShowCurrentStatementFrame.value = settingsStore.editorSettings.showCurrentStatementFrame; @@ -895,8 +889,6 @@ function hasChanges(): boolean { async function persistSettings() { if (hasApplyBlocker.value) return; const editorSettingsPatch = editorSettingsPatchFromDraft(currentEditorSettingsDraft(), editEditorSettingsBase.value); - const globalConnectTimeoutChanged = editorSettingsPatch.globalConnectTimeoutSecs !== undefined; - const globalQueryTimeoutChanged = editorSettingsPatch.globalQueryTimeoutSecs !== undefined; const sidebarObjectDisplayChanged = editorSettingsPatch.sidebarObjectDisplay !== undefined && editorSettingsPatch.sidebarObjectDisplay !== settingsStore.editorSettings.sidebarObjectDisplay; const sidebarTablePageSizeChanged = editSidebarTablePageSize.value !== (settingsStore.desktopSettings.sidebar_table_page_size ?? DEFAULT_SIDEBAR_TABLE_PAGE_SIZE); if (Object.keys(editorSettingsPatch).length > 0) { @@ -904,12 +896,6 @@ async function persistSettings() { await settingsStore.persistEditorSettings(); editEditorSettingsBase.value = editorSettingsDraftFromSettings(settingsStore.editorSettings); } - if (globalConnectTimeoutChanged || globalQueryTimeoutChanged) { - await connectionStore.applyGlobalTimeouts({ - connectTimeoutSecs: globalConnectTimeoutChanged ? settingsStore.editorSettings.globalConnectTimeoutSecs : undefined, - queryTimeoutSecs: globalQueryTimeoutChanged ? settingsStore.editorSettings.globalQueryTimeoutSecs : undefined, - }); - } await settingsStore.updateDesktopSettings({ show_tray_icon: editShowTrayIcon.value, quit_on_close: editQuitOnClose.value, @@ -957,8 +943,6 @@ function resetDefaultsForTab(tab: SettingsCategory) { editFontSize.value = DEFAULT_EDITOR_SETTINGS.fontSize; editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode; editExecuteAllOnBlankLine.value = DEFAULT_EDITOR_SETTINGS.executeAllOnBlankLine; - editGlobalConnectTimeoutSecs.value = DEFAULT_EDITOR_SETTINGS.globalConnectTimeoutSecs; - editGlobalQueryTimeoutSecs.value = DEFAULT_EDITOR_SETTINGS.globalQueryTimeoutSecs; editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker; editShowStatementRunButtons.value = DEFAULT_EDITOR_SETTINGS.showStatementRunButtons; editShowCurrentStatementFrame.value = DEFAULT_EDITOR_SETTINGS.showCurrentStatementFrame; @@ -1055,8 +1039,6 @@ function resetAllDefaults() { editActiveCustomThemeId.value = DEFAULT_EDITOR_SETTINGS.activeCustomThemeId; editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode; editExecuteAllOnBlankLine.value = DEFAULT_EDITOR_SETTINGS.executeAllOnBlankLine; - editGlobalConnectTimeoutSecs.value = DEFAULT_EDITOR_SETTINGS.globalConnectTimeoutSecs; - editGlobalQueryTimeoutSecs.value = DEFAULT_EDITOR_SETTINGS.globalQueryTimeoutSecs; editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker; editShowStatementRunButtons.value = DEFAULT_EDITOR_SETTINGS.showStatementRunButtons; editShowCurrentStatementFrame.value = DEFAULT_EDITOR_SETTINGS.showCurrentStatementFrame; @@ -3698,26 +3680,6 @@ onUnmounted(() => { -
-
- -

- {{ t("settings.globalConnectTimeoutDescription") }} -

-
- -
- -
-
- -

- {{ t("settings.globalQueryTimeoutDescription") }} -

-
- -
-
diff --git a/apps/desktop/src/composables/__tests__/useQuickOpen.spec.ts b/apps/desktop/src/composables/__tests__/useQuickOpen.spec.ts index 071f70f584..212627ba45 100644 --- a/apps/desktop/src/composables/__tests__/useQuickOpen.spec.ts +++ b/apps/desktop/src/composables/__tests__/useQuickOpen.spec.ts @@ -1,6 +1,6 @@ import { nextTick } from "vue"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { useQuickOpen } from "@/composables/useQuickOpen"; +import { matchQuickOpenText, useQuickOpen } from "@/composables/useQuickOpen"; import * as api from "@/lib/backend/api"; import { getSqlFileFolderPaths, sqlFileFoldersVersion } from "@/lib/sqlFile/sqlFileFolders"; import { useConnectionStore } from "@/stores/connectionStore"; @@ -84,6 +84,68 @@ describe("useQuickOpen", () => { }); describe("fuzzyMatch function", () => { + it("ranks exact names, initials, prefixes, substrings, and fuzzy matches in order", () => { + const exact = matchQuickOpenText("shop", "shop"); + const initials = matchQuickOpenText("gafi", "groupon_apply_finance_invoice"); + const prefix = matchQuickOpenText("shop", "shop_attribute"); + const substring = matchQuickOpenText("shop", "sale_payway_shop"); + const fuzzy = matchQuickOpenText("gafi", "giftcard_define_item"); + + expect([exact?.kind, initials?.kind, prefix?.kind, substring?.kind, fuzzy?.kind]).toEqual(["exact", "initials", "prefix", "substring", "fuzzy"]); + expect(exact!.score).toBeLessThan(initials!.score); + expect(initials!.score).toBeLessThan(prefix!.score); + expect(prefix!.score).toBeLessThan(substring!.score); + expect(substring!.score).toBeLessThan(fuzzy!.score); + expect(initials?.indices).toEqual([0, 8, 14, 22]); + }); + + it("matches multi-word prefix combinations and highlights their source characters", () => { + const label = "giftcard_define_shop_log"; + + expect(matchQuickOpenText("gdsl", label)?.kind).toBe("initials"); + for (const query of ["giftdsl", "gdshopl", "gdesl"]) { + const match = matchQuickOpenText(query, label); + expect(match?.kind).toBe("word-prefix"); + expect(match?.indices.map((index) => label[index]).join("")).toBe(query); + } + expect(matchQuickOpenText("giftcard", label)?.kind).toBe("prefix"); + expect(matchQuickOpenText("define", label)?.kind).toBe("substring"); + expect(matchQuickOpenText("shop", label)?.kind).toBe("substring"); + expect(matchQuickOpenText("cardshoplog", label)?.kind).toBe("fuzzy"); + }); + + it("puts the exact shop result before prefixed and containing names", () => { + vi.mocked(useConnectionStore).mockReturnValue({ + connections: [ + { id: "contains", name: "sale_payway_shop", type: "mssql" }, + { id: "prefix", name: "shop_attribute", type: "mssql" }, + { id: "exact", name: "shop", type: "mssql" }, + ], + treeNodes: [], + } as any); + + const { filteredItems, setQuery } = useQuickOpen(); + setQuery("shop"); + + expect(filteredItems.value.map((item) => item.label)).toEqual(["shop", "shop_attribute", "sale_payway_shop"]); + }); + + it("puts an exact identifier acronym before loose fuzzy matches", () => { + vi.mocked(useConnectionStore).mockReturnValue({ + connections: [ + { id: "fuzzy", name: "giftcard_define_item", type: "mssql" }, + { id: "initials", name: "groupon_apply_finance_invoice", type: "mssql" }, + ], + treeNodes: [], + } as any); + + const { filteredItems, setQuery } = useQuickOpen(); + setQuery("gafi"); + + expect(filteredItems.value.map((item) => item.label)).toEqual(["groupon_apply_finance_invoice", "giftcard_define_item"]); + expect(filteredItems.value[0].matchIndices).toEqual([0, 8, 14, 22]); + }); + it("should return exact substring match with score 1", () => { // Mock store with test data const mockStore = { @@ -186,6 +248,66 @@ describe("useQuickOpen", () => { }); describe("filtering and searching", () => { + it("keeps database-object highlight indices relative to the visible label", () => { + vi.mocked(useConnectionStore).mockReturnValue({ + connections: [{ id: "conn1", name: "Test TiDB", db_type: "mysql" }], + treeNodes: [ + { + id: "conn1", + connectionId: "conn1", + type: "connection", + label: "Test TiDB", + children: [ + { + id: "conn1:retail_mps", + connectionId: "conn1", + type: "database", + database: "retail_mps", + label: "retail_mps", + children: [ + { + id: "table1", + connectionId: "conn1", + type: "table", + database: "retail_mps", + label: "groupon_apply_finance_invoice", + }, + ], + }, + ], + }, + ], + } as any); + + const { filteredItems, setQuery } = useQuickOpen(); + setQuery("gafi"); + + const table = filteredItems.value.find((item) => item.type === "table"); + expect(table?.label).toBe("groupon_apply_finance_invoice"); + expect(table?.matchIndices).toEqual([0, 8, 14, 22]); + }); + + it("does not highlight the label when only connection metadata matches", () => { + vi.mocked(useConnectionStore).mockReturnValue({ + connections: [{ id: "conn1", name: "ProdConnection", db_type: "mysql" }], + treeNodes: [ + { + connectionId: "conn1", + type: "database", + database: "UserDB", + label: "UserDB", + }, + ], + } as any); + + const { filteredItems, setQuery } = useQuickOpen(); + setQuery("Prod"); + + const database = filteredItems.value.find((item) => item.type === "database"); + expect(database?.matchIndices).toEqual([]); + expect(database?.matchScore).toBeGreaterThan(1000); + }); + it("indexes database objects under connection groups", () => { const mockStore = { connections: [{ id: "conn1", name: "Grouped PG", db_type: "postgres" }], @@ -503,6 +625,23 @@ describe("useQuickOpen", () => { }); describe("reset and query setting", () => { + it("resets selection when v-model changes the search query directly", () => { + vi.mocked(useConnectionStore).mockReturnValue({ + connections: [ + { id: "conn1", name: "Connection1", type: "mssql" }, + { id: "conn2", name: "Connection2", type: "mssql" }, + ], + treeNodes: [], + } as any); + + const { searchQuery, selectNext, selectedIndex } = useQuickOpen(); + selectNext(); + expect(selectedIndex.value).toBe(1); + + searchQuery.value = "Connection"; + expect(selectedIndex.value).toBe(0); + }); + it("should reset selection to 0 when setQuery is called", () => { const mockStore = { connections: [ diff --git a/apps/desktop/src/composables/useQuickOpen.ts b/apps/desktop/src/composables/useQuickOpen.ts index 0c417d849e..f4719882a1 100644 --- a/apps/desktop/src/composables/useQuickOpen.ts +++ b/apps/desktop/src/composables/useQuickOpen.ts @@ -37,45 +37,141 @@ export interface QuickOpenItem { sqlFileId?: string; // For saved SQL library files } -/** - * Fuzzy match function that checks if query matches text - * Returns the matched indices for highlighting - */ -function fuzzyMatch(query: string, text: string): { score: number; indices: number[] } | null { - const lowerQuery = query.toLowerCase(); - const lowerText = text.toLowerCase(); +export type QuickOpenMatchKind = "exact" | "initials" | "prefix" | "word-prefix" | "substring" | "fuzzy"; - if (!lowerQuery) return { score: Infinity, indices: [] }; - if (lowerText.includes(lowerQuery)) { - // Exact substring match gets highest score - const startIdx = lowerText.indexOf(lowerQuery); - return { - score: 1, - indices: Array.from({ length: lowerQuery.length }, (_, i) => startIdx + i), - }; +export interface QuickOpenMatch { + kind: QuickOpenMatchKind; + score: number; + indices: number[]; +} + +interface IdentifierWord { + text: string; + start: number; +} + +const IDENTIFIER_SEPARATOR_RE = /[_\-. /\\]/; + +function identifierWords(text: string): IdentifierWord[] { + const words: IdentifierWord[] = []; + let start = -1; + + function pushWord(end: number): void { + if (start < 0 || end <= start) return; + words.push({ text: text.slice(start, end), start }); } - // Fuzzy match: find all characters in order - let queryIdx = 0; - const indices: number[] = []; - let score = 0; - let lastMatchIdx = -1; - - for (let i = 0; i < lowerText.length && queryIdx < lowerQuery.length; i++) { - if (lowerText[i] === lowerQuery[queryIdx]) { - indices.push(i); - // Score based on proximity (consecutive chars score better) - score += lastMatchIdx === i - 1 ? 2 : 1; - lastMatchIdx = i; - queryIdx++; + for (let index = 0; index < text.length; index++) { + const char = text[index]; + if (IDENTIFIER_SEPARATOR_RE.test(char)) { + pushWord(index); + start = -1; + continue; + } + if (start < 0) { + start = index; + continue; + } + const previous = text[index - 1]; + if (previous >= "a" && previous <= "z" && char >= "A" && char <= "Z") { + pushWord(index); + start = index; } } + pushWord(text.length); + return words; +} - if (queryIdx === lowerQuery.length) { - return { score: score / lowerQuery.length, indices }; +function rangeIndices(start: number, length: number): number[] { + return Array.from({ length }, (_, index) => start + index); +} + +function matchWordPrefixes(words: IdentifierWord[], query: string): number[] | null { + interface PrefixState { + queryIndex: number; + firstWordIndex: number; + lastWordIndex: number; + usedWords: number; + indices: number[]; } - return null; + function stateScore(state: PrefixState): number { + if (state.usedWords === 0) return 0; + return (state.lastWordIndex - state.firstWordIndex - state.usedWords + 1) * 10 + state.usedWords; + } + + function retainBest(states: Map, candidate: PrefixState): void { + const key = `${candidate.queryIndex}:${candidate.usedWords}`; + const current = states.get(key); + if (!current || stateScore(candidate) < stateScore(current)) states.set(key, candidate); + } + + let states = new Map([["0:0", { queryIndex: 0, firstWordIndex: -1, lastWordIndex: -1, usedWords: 0, indices: [] }]]); + for (let wordIndex = 0; wordIndex < words.length; wordIndex++) { + const nextStates = new Map(states); + const word = words[wordIndex]; + const lowerWord = word.text.toLowerCase(); + for (const state of states.values()) { + const maxLength = Math.min(lowerWord.length, query.length - state.queryIndex); + for (let length = 1; length <= maxLength; length++) { + if (lowerWord.slice(0, length) !== query.slice(state.queryIndex, state.queryIndex + length)) break; + retainBest(nextStates, { + queryIndex: state.queryIndex + length, + firstWordIndex: state.usedWords === 0 ? wordIndex : state.firstWordIndex, + lastWordIndex: wordIndex, + usedWords: state.usedWords + 1, + indices: [...state.indices, ...rangeIndices(word.start, length)], + }); + } + } + states = nextStates; + } + + return [...states.values()].filter((state) => state.queryIndex === query.length && state.usedWords >= 2).sort((a, b) => stateScore(a) - stateScore(b))[0]?.indices ?? null; +} + +/** Match one quick-open field and return label-relative highlight indices. */ +export function matchQuickOpenText(query: string, text: string): QuickOpenMatch | null { + const lowerQuery = query.trim().toLowerCase(); + const lowerText = text.toLowerCase(); + if (!lowerQuery) return { kind: "exact", score: Infinity, indices: [] }; + + if (lowerText === lowerQuery) { + return { kind: "exact", score: 1, indices: rangeIndices(0, text.length) }; + } + + const words = identifierWords(text); + const initials = words.map((word) => word.text[0]?.toLowerCase() ?? "").join(""); + if (words.length >= 2 && initials === lowerQuery) { + return { kind: "initials", score: 100 + Math.min(words.length, 99), indices: words.map((word) => word.start) }; + } + + if (lowerText.startsWith(lowerQuery)) { + return { kind: "prefix", score: 200 + Math.min(text.length - lowerQuery.length, 99), indices: rangeIndices(0, lowerQuery.length) }; + } + + const wordPrefixIndices = matchWordPrefixes(words, lowerQuery); + if (wordPrefixIndices) { + return { kind: "word-prefix", score: 300 + Math.min(text.length - lowerQuery.length, 99), indices: wordPrefixIndices }; + } + + const substringIndex = lowerText.indexOf(lowerQuery); + if (substringIndex >= 0) { + return { kind: "substring", score: 400 + Math.min(substringIndex, 99), indices: rangeIndices(substringIndex, lowerQuery.length) }; + } + + if (lowerQuery.length < 2) return null; + const indices: number[] = []; + let queryIndex = 0; + for (let index = 0; index < lowerText.length && queryIndex < lowerQuery.length; index++) { + if (lowerText[index] !== lowerQuery[queryIndex]) continue; + indices.push(index); + queryIndex++; + } + if (queryIndex !== lowerQuery.length) return null; + + const span = indices[indices.length - 1] - indices[0] + 1; + return { kind: "fuzzy", score: 500 + Math.min(span - lowerQuery.length, 99), indices }; } interface MatchedItem extends QuickOpenItem { @@ -574,6 +670,7 @@ export function useQuickOpen() { watch( searchQuery, (query) => { + selectedIndex.value = 0; const generation = ++remoteSearchGeneration; cancelStaleRemoteRequestWaiters(generation); if (remoteSearchTimer) clearTimeout(remoteSearchTimer); @@ -616,12 +713,14 @@ export function useQuickOpen() { const key = quickOpenItemKey(item); if (seen.has(key)) continue; seen.add(key); - const result = fuzzyMatch(searchQuery.value, item.searchText); + const labelMatch = matchQuickOpenText(searchQuery.value, item.label); + const metadataMatch = labelMatch ? null : matchQuickOpenText(searchQuery.value, item.searchText); + const result = labelMatch ?? metadataMatch; if (result) { matched.push({ ...item, - matchScore: result.score, - matchIndices: result.indices, + matchScore: result.score + (labelMatch ? 0 : 1000), + matchIndices: labelMatch ? result.indices : [], }); } } @@ -647,7 +746,11 @@ export function useQuickOpen() { sql_library_file: 11, sql_file: 12, }; - return typeOrder[a.type] - typeOrder[b.type]; + const typeDifference = typeOrder[a.type] - typeOrder[b.type]; + if (typeDifference !== 0) return typeDifference; + const lengthDifference = a.label.length - b.label.length; + if (lengthDifference !== 0) return lengthDifference; + return a.label.localeCompare(b.label); }); return matched.slice(0, QUICK_OPEN_MAX_RESULTS); diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 7b57425c97..8ea4c47c45 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -5736,7 +5736,7 @@ export default { shortcutNewQuery: "New query", shortcutOpenSettings: "Open settings", shortcutCloseTab: "Close tab", - shortcutFocusSearch: "Focus search", + shortcutFocusSearch: "Focus current view search", shortcutQuickOpen: "Quick open (search all database objects)", shortcutSwitchToPreviousTab: "Switch to previous tab", shortcutSwitchToNextTab: "Switch to next tab", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 268aae0ee5..a1090662a9 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -5454,7 +5454,7 @@ export default withEnglishFallback({ shortcutOpenSettings: "Abrir configuración", shortcutCloseTab: "Cerrar pestaña", shortcutToggleSidebar: "Alternar barra lateral", - shortcutFocusSearch: "Enfocar búsqueda", + shortcutFocusSearch: "Enfocar búsqueda de la vista actual", shortcutQuickOpen: "Abrir rápido (buscar todos los objetos de base de datos)", shortcutSwitchToPreviousTab: "Cambiar a la pestaña anterior", shortcutSwitchToNextTab: "Cambiar a la pestaña siguiente", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 201ad74efe..721f41ff9d 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -5453,7 +5453,7 @@ export default withEnglishFallback({ shortcutNewQuery: "Nuova query", shortcutOpenSettings: "Apri impostazioni", shortcutCloseTab: "Chiudi scheda", - shortcutFocusSearch: "Focalizza ricerca", + shortcutFocusSearch: "Focalizza la ricerca nella vista corrente", shortcutQuickOpen: "Apertura rapida (ricerca tutti gli oggetti del database)", shortcutSwitchToPreviousTab: "Passa alla scheda precedente", shortcutSwitchToNextTab: "Passa alla scheda successiva", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index b7d3de9cc2..e5e0be1184 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -5472,7 +5472,7 @@ export default withEnglishFallback({ shortcutNewQuery: "新しいクエリ", shortcutOpenSettings: "設定を開く", shortcutCloseTab: "タブを閉じる", - shortcutFocusSearch: "検索にフォーカス", + shortcutFocusSearch: "現在のビューの検索にフォーカス", shortcutQuickOpen: "クイックオープン (すべてのデータベースオブジェクトを検索)", shortcutSwitchToPreviousTab: "前のタブに切り替え", shortcutSwitchToNextTab: "次のタブに切り替え", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 23ebb5456d..7d94d4d265 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -5220,7 +5220,7 @@ export default withEnglishFallback({ shortcutNewQuery: "새 쿼리", shortcutOpenSettings: "설정 열기", shortcutCloseTab: "탭 닫기", - shortcutFocusSearch: "검색에 포커스", + shortcutFocusSearch: "현재 화면 검색에 포커스", shortcutQuickOpen: "빠른 열기 (모든 데이터베이스 객체 검색)", shortcutSwitchToPreviousTab: "이전 탭으로 전환", shortcutSwitchToNextTab: "다음 탭으로 전환", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 48b795eb50..3315c9e6f2 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -5456,7 +5456,7 @@ export default withEnglishFallback({ shortcutOpenSettings: "Abrir configurações", shortcutCloseTab: "Fechar aba", shortcutToggleSidebar: "Alternar barra lateral", - shortcutFocusSearch: "Focar na pesquisa", + shortcutFocusSearch: "Focar na pesquisa da tela atual", shortcutQuickOpen: "Abrir rapidamente (pesquisar todos os objetos do banco de dados)", shortcutSwitchToPreviousTab: "Alternar para a aba anterior", shortcutSwitchToNextTab: "Alternar para a próxima aba", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 2eceba2e7f..b29804a8cf 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -5661,7 +5661,7 @@ export default withEnglishFallback({ shortcutOpenSettings: "打开设置", shortcutCloseTab: "关闭标签页", shortcutToggleSidebar: "切换侧边栏", - shortcutFocusSearch: "聚焦搜索", + shortcutFocusSearch: "聚焦当前页面搜索", shortcutQuickOpen: "快速打开 (搜索所有数据库对象)", shortcutSwitchToPreviousTab: "切换到左侧标签页", shortcutSwitchToNextTab: "切换到右侧标签页", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index ebb04eb1af..15178228e8 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -4889,7 +4889,7 @@ export default withEnglishFallback({ shortcutOpenSettings: "開啟設定", shortcutCloseTab: "關閉分頁", shortcutToggleSidebar: "切換側邊欄", - shortcutFocusSearch: "聚焦搜尋", + shortcutFocusSearch: "聚焦目前頁面搜尋", shortcutQuickOpen: "快速開啟 (搜尋所有資料庫物件)", shortcutSwitchToPreviousTab: "切換到左側分頁", shortcutSwitchToNextTab: "切換到右側分頁", diff --git a/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts b/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts index e9dfe9bdf4..714bf5b9d8 100644 --- a/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts +++ b/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts @@ -50,6 +50,16 @@ describe("shortcutRegistry editor actions", () => { expect(findShortcutConflict("expandSelectStar", DEFAULT_SHORTCUT_SETTINGS.expandSelectStar, DEFAULT_SHORTCUT_SETTINGS)).toBeNull(); }); + it("keeps current-view search and editor find contextual on Mod+F", () => { + const focusSearch = SHORTCUT_DEFINITIONS.find((item) => item.id === "focusSearch"); + const find = SHORTCUT_DEFINITIONS.find((item) => item.id === "find"); + + expect(focusSearch).toMatchObject({ scope: "global", defaultShortcut: "Mod+F" }); + expect(find).toMatchObject({ scope: "editor", defaultShortcut: "Mod+F" }); + expect(findShortcutConflict("focusSearch", DEFAULT_SHORTCUT_SETTINGS.focusSearch, DEFAULT_SHORTCUT_SETTINGS)).toBeNull(); + expect(findShortcutConflict("find", DEFAULT_SHORTCUT_SETTINGS.find, DEFAULT_SHORTCUT_SETTINGS)).toBeNull(); + }); + it("uses Shift+Enter for inserting a complete line below", () => { const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === "insertLineBelow"); diff --git a/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts b/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts index 0f35a77ef4..65b049044d 100644 --- a/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts +++ b/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts @@ -32,6 +32,11 @@ function makeSettings(overrides: Partial = {}): EditorSettings { } describe("EDITOR_SETTINGS_DRAFT_KEYS", () => { + it("keeps connection and query timeout ownership outside editor settings", () => { + expect(EDITOR_SETTINGS_DRAFT_KEYS).not.toContain("globalConnectTimeoutSecs"); + expect(EDITOR_SETTINGS_DRAFT_KEYS).not.toContain("globalQueryTimeoutSecs"); + }); + it("includes continueOnErrorOnBatch", () => { expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("continueOnErrorOnBatch"); }); @@ -61,6 +66,16 @@ describe("EDITOR_SETTINGS_DRAFT_KEYS", () => { }); describe("editorSettingsDraftFromSettings", () => { + it("does not include persisted global timeout values in editor drafts", () => { + const settings = makeSettings({ globalConnectTimeoutSecs: 17, globalQueryTimeoutSecs: 43 }); + const draft = editorSettingsDraftFromSettings(settings); + + expect(draft).not.toHaveProperty("globalConnectTimeoutSecs"); + expect(draft).not.toHaveProperty("globalQueryTimeoutSecs"); + expect(editorSettingsPatchFromDraft(draft, draft)).not.toHaveProperty("globalConnectTimeoutSecs"); + expect(editorSettingsPatchFromDraft(draft, draft)).not.toHaveProperty("globalQueryTimeoutSecs"); + }); + it("maps continueOnErrorOnBatch from settings", () => { const draft = editorSettingsDraftFromSettings(makeSettings({ continueOnErrorOnBatch: true })); expect(draft.continueOnErrorOnBatch).toBe(true); diff --git a/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts b/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts index 4e61498e01..84fed15bd3 100644 --- a/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts +++ b/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts @@ -37,6 +37,11 @@ describe("settings search", () => { { id: "desktop", category: "about", titleKey: "hidden", visible: ({ isWeb }) => !isWeb }, ]; + it("does not index connection or query timeout under editor settings", () => { + expect(SETTINGS_SEARCH_DEFINITIONS.map((definition) => definition.id)).not.toContain("editor-global-connect-timeout"); + expect(SETTINGS_SEARCH_DEFINITIONS.map((definition) => definition.id)).not.toContain("editor-global-query-timeout"); + }); + it("matches translated title, description, and category without changing declared order", () => { const entries = resolveSettingsSearchEntries(definitions, { isWeb: false, visibleCategories: allCategories }, translate, categoryLabels); expect(searchSettings(entries, "TYPEFACE", "en").map((entry) => entry.id)).toEqual(["font"]); diff --git a/apps/desktop/src/lib/settings/editorSettingsDraft.ts b/apps/desktop/src/lib/settings/editorSettingsDraft.ts index 6a154cd7d6..458c7b69a6 100644 --- a/apps/desktop/src/lib/settings/editorSettingsDraft.ts +++ b/apps/desktop/src/lib/settings/editorSettingsDraft.ts @@ -14,8 +14,6 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [ "activeCustomThemeId", "executeMode", "executeAllOnBlankLine", - "globalConnectTimeoutSecs", - "globalQueryTimeoutSecs", "showExecutionTargetPicker", "showStatementRunButtons", "showCurrentStatementFrame", diff --git a/apps/desktop/src/lib/settings/settingsSearch.ts b/apps/desktop/src/lib/settings/settingsSearch.ts index 6e9de4e8d6..3b4036c9cf 100644 --- a/apps/desktop/src/lib/settings/settingsSearch.ts +++ b/apps/desktop/src/lib/settings/settingsSearch.ts @@ -107,8 +107,6 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] = { id: "editor-font-size", category: "editor", titleKey: "settings.fontSize", targetId: "editor" }, { id: "editor-execute-mode", category: "editor", titleKey: "settings.executeMode", targetId: "editor" }, { id: "editor-execute-all-on-blank-line", category: "editor", titleKey: "settings.executeAllOnBlankLine", descriptionKey: "settings.executeAllOnBlankLineDescription", targetId: "editor" }, - { id: "editor-global-connect-timeout", category: "editor", titleKey: "settings.globalConnectTimeout", descriptionKey: "settings.globalConnectTimeoutDescription", targetId: "editor" }, - { id: "editor-global-query-timeout", category: "editor", titleKey: "settings.globalQueryTimeout", descriptionKey: "settings.globalQueryTimeoutDescription", targetId: "editor" }, { id: "editor-execution-target", category: "editor", titleKey: "settings.showExecutionTargetPicker", descriptionKey: "settings.showExecutionTargetPickerDescription", targetId: "editor" }, { id: "editor-run-buttons", category: "editor", titleKey: "settings.showStatementRunButtons", descriptionKey: "settings.showStatementRunButtonsDescription", targetId: "editor" }, { id: "editor-statement-frame", category: "editor", titleKey: "settings.showCurrentStatementFrame", descriptionKey: "settings.showCurrentStatementFrameDescription", targetId: "editor" },