fix(search): improve quick-open ranking and simplify settings
This commit is contained in:
16 files changed
+316
-86
No files matched your search
@@ -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(() => {
|
||||
<Switch id="editor-execute-all-on-blank-line" v-model="editExecuteAllOnBlankLine" :disabled="editExecuteMode !== 'current'" class="mt-0.5" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="editor-global-connect-timeout">{{ t("settings.globalConnectTimeout") }}</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t("settings.globalConnectTimeoutDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
<Input id="editor-global-connect-timeout" v-model.number="editGlobalConnectTimeoutSecs" type="number" min="1" max="300" step="1" class="w-24" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="editor-global-query-timeout">{{ t("settings.globalQueryTimeout") }}</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t("settings.globalQueryTimeoutDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
<Input id="editor-global-query-timeout" v-model.number="editGlobalQueryTimeoutSecs" type="number" min="0" max="300" step="1" class="w-24" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="editor-show-execution-target-picker">{{ t("settings.showExecutionTargetPicker") }}</Label>
|
||||
|
||||
+140
-1
@@ -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: [
|
||||
|
||||
@@ -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<string, PrefixState>, 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<string, PrefixState>([["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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -5472,7 +5472,7 @@ export default withEnglishFallback({
|
||||
shortcutNewQuery: "新しいクエリ",
|
||||
shortcutOpenSettings: "設定を開く",
|
||||
shortcutCloseTab: "タブを閉じる",
|
||||
shortcutFocusSearch: "検索にフォーカス",
|
||||
shortcutFocusSearch: "現在のビューの検索にフォーカス",
|
||||
shortcutQuickOpen: "クイックオープン (すべてのデータベースオブジェクトを検索)",
|
||||
shortcutSwitchToPreviousTab: "前のタブに切り替え",
|
||||
shortcutSwitchToNextTab: "次のタブに切り替え",
|
||||
|
||||
@@ -5220,7 +5220,7 @@ export default withEnglishFallback({
|
||||
shortcutNewQuery: "새 쿼리",
|
||||
shortcutOpenSettings: "설정 열기",
|
||||
shortcutCloseTab: "탭 닫기",
|
||||
shortcutFocusSearch: "검색에 포커스",
|
||||
shortcutFocusSearch: "현재 화면 검색에 포커스",
|
||||
shortcutQuickOpen: "빠른 열기 (모든 데이터베이스 객체 검색)",
|
||||
shortcutSwitchToPreviousTab: "이전 탭으로 전환",
|
||||
shortcutSwitchToNextTab: "다음 탭으로 전환",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -5661,7 +5661,7 @@ export default withEnglishFallback({
|
||||
shortcutOpenSettings: "打开设置",
|
||||
shortcutCloseTab: "关闭标签页",
|
||||
shortcutToggleSidebar: "切换侧边栏",
|
||||
shortcutFocusSearch: "聚焦搜索",
|
||||
shortcutFocusSearch: "聚焦当前页面搜索",
|
||||
shortcutQuickOpen: "快速打开 (搜索所有数据库对象)",
|
||||
shortcutSwitchToPreviousTab: "切换到左侧标签页",
|
||||
shortcutSwitchToNextTab: "切换到右侧标签页",
|
||||
|
||||
@@ -4889,7 +4889,7 @@ export default withEnglishFallback({
|
||||
shortcutOpenSettings: "開啟設定",
|
||||
shortcutCloseTab: "關閉分頁",
|
||||
shortcutToggleSidebar: "切換側邊欄",
|
||||
shortcutFocusSearch: "聚焦搜尋",
|
||||
shortcutFocusSearch: "聚焦目前頁面搜尋",
|
||||
shortcutQuickOpen: "快速開啟 (搜尋所有資料庫物件)",
|
||||
shortcutSwitchToPreviousTab: "切換到左側分頁",
|
||||
shortcutSwitchToNextTab: "切換到右側分頁",
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -32,6 +32,11 @@ function makeSettings(overrides: Partial<EditorSettings> = {}): 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);
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -14,8 +14,6 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
||||
"activeCustomThemeId",
|
||||
"executeMode",
|
||||
"executeAllOnBlankLine",
|
||||
"globalConnectTimeoutSecs",
|
||||
"globalQueryTimeoutSecs",
|
||||
"showExecutionTargetPicker",
|
||||
"showStatementRunButtons",
|
||||
"showCurrentStatementFrame",
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in new issue
Block a user