mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
@@ -281,6 +281,7 @@ const editShowCurrentStatementFrame = ref(settingsStore.editorSettings.showCurre
|
||||
const editShowInsertValueHints = ref(settingsStore.editorSettings.showInsertValueHints);
|
||||
const editAutoAliasTables = ref(settingsStore.editorSettings.autoAliasTables);
|
||||
const editInsertSpaceAfterCompletion = ref(settingsStore.editorSettings.insertSpaceAfterCompletion);
|
||||
const editSortCompletionColumnsAlphabetically = ref(settingsStore.editorSettings.sortCompletionColumnsAlphabetically);
|
||||
const editCompletionTriggerMode = ref<SqlCompletionTriggerMode>(settingsStore.editorSettings.completionTriggerMode);
|
||||
const editWordWrap = ref(settingsStore.editorSettings.wordWrap);
|
||||
const editVimModeEnabled = ref(settingsStore.editorSettings.vimModeEnabled);
|
||||
@@ -472,6 +473,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
|
||||
showInsertValueHints: editShowInsertValueHints.value,
|
||||
autoAliasTables: editAutoAliasTables.value,
|
||||
insertSpaceAfterCompletion: editInsertSpaceAfterCompletion.value,
|
||||
sortCompletionColumnsAlphabetically: editSortCompletionColumnsAlphabetically.value,
|
||||
completionTriggerMode: editCompletionTriggerMode.value,
|
||||
wordWrap: editWordWrap.value,
|
||||
vimModeEnabled: editVimModeEnabled.value,
|
||||
@@ -746,6 +748,7 @@ function syncEditorSettingsDraftFromStore() {
|
||||
editShowInsertValueHints.value = settingsStore.editorSettings.showInsertValueHints;
|
||||
editAutoAliasTables.value = settingsStore.editorSettings.autoAliasTables;
|
||||
editInsertSpaceAfterCompletion.value = settingsStore.editorSettings.insertSpaceAfterCompletion;
|
||||
editSortCompletionColumnsAlphabetically.value = settingsStore.editorSettings.sortCompletionColumnsAlphabetically;
|
||||
editCompletionTriggerMode.value = settingsStore.editorSettings.completionTriggerMode;
|
||||
editWordWrap.value = settingsStore.editorSettings.wordWrap;
|
||||
editVimModeEnabled.value = settingsStore.editorSettings.vimModeEnabled;
|
||||
@@ -958,6 +961,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
|
||||
editShowInsertValueHints.value = DEFAULT_EDITOR_SETTINGS.showInsertValueHints;
|
||||
editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables;
|
||||
editInsertSpaceAfterCompletion.value = DEFAULT_EDITOR_SETTINGS.insertSpaceAfterCompletion;
|
||||
editSortCompletionColumnsAlphabetically.value = DEFAULT_EDITOR_SETTINGS.sortCompletionColumnsAlphabetically;
|
||||
editCompletionTriggerMode.value = DEFAULT_EDITOR_SETTINGS.completionTriggerMode;
|
||||
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
|
||||
editVimModeEnabled.value = DEFAULT_EDITOR_SETTINGS.vimModeEnabled;
|
||||
@@ -1057,6 +1061,7 @@ function resetAllDefaults() {
|
||||
editShowInsertValueHints.value = DEFAULT_EDITOR_SETTINGS.showInsertValueHints;
|
||||
editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables;
|
||||
editInsertSpaceAfterCompletion.value = DEFAULT_EDITOR_SETTINGS.insertSpaceAfterCompletion;
|
||||
editSortCompletionColumnsAlphabetically.value = DEFAULT_EDITOR_SETTINGS.sortCompletionColumnsAlphabetically;
|
||||
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
|
||||
editVimModeEnabled.value = DEFAULT_EDITOR_SETTINGS.vimModeEnabled;
|
||||
editAutoCloseBrackets.value = DEFAULT_EDITOR_SETTINGS.autoCloseBrackets;
|
||||
@@ -3791,6 +3796,16 @@ onUnmounted(() => {
|
||||
<Switch id="editor-insert-space-after-completion" v-model="editInsertSpaceAfterCompletion" 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-sort-completion-columns-alphabetically">{{ t("settings.sortCompletionColumnsAlphabetically") }}</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t("settings.sortCompletionColumnsAlphabeticallyDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
<Switch id="editor-sort-completion-columns-alphabetically" v-model="editSortCompletionColumnsAlphabetically" 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-auto-alias-tables">{{ t("settings.autoAliasTables") }}</Label>
|
||||
|
||||
@@ -101,7 +101,7 @@ import { EDITOR_FONT_FAMILY_CSS_VAR, EDITOR_FONT_SIZE_CSS_VAR, loadEditorTheme,
|
||||
import { createStatementGutterMarkerDom, shouldShowStatementGutter } from "@/lib/editor/codemirrorStatementGutter";
|
||||
import { createQueryEditorSearchKeymap } from "@/lib/editor/queryEditorSearchKeymap";
|
||||
import { appendSqlCompletionSpace } from "@/lib/editor/sqlCompletionInsertion";
|
||||
import { completionLabelPresentation } from "@/lib/editor/sqlCompletionPresentation";
|
||||
import { compareSqlCompletions, completionLabelPresentation } from "@/lib/editor/sqlCompletionPresentation";
|
||||
import { clampEditorFontSize, createEditorZoomCommitScheduler, fontSizeFromGestureScale, fontSizeFromWheelDelta } from "@/lib/editor/editorZoom";
|
||||
import { normalizeShortcutSettings, shortcutToCodeMirrorKey } from "@/lib/editor/shortcutRegistry";
|
||||
import { trimmedSelectionLayer } from "@/lib/editor/codemirrorTrimmedSelectionLayer";
|
||||
@@ -4587,6 +4587,7 @@ onMounted(async () => {
|
||||
buildSqlCompletionExtension = () =>
|
||||
autocompletion({
|
||||
activateOnTyping: true,
|
||||
compareCompletions: (a, b) => compareSqlCompletions(a, b, settingsStore.editorSettings.sortCompletionColumnsAlphabetically),
|
||||
override: [async (context: CompletionContext) => provideSqlCompletions(context)],
|
||||
});
|
||||
|
||||
@@ -5400,7 +5401,7 @@ watch(
|
||||
);
|
||||
|
||||
watch(
|
||||
() => settingsStore.editorSettings.snippets,
|
||||
() => [settingsStore.editorSettings.snippets, settingsStore.editorSettings.sortCompletionColumnsAlphabetically],
|
||||
() => {
|
||||
completionEpoch++;
|
||||
if (!view.value || !completionComp || !buildSqlCompletionExtension) return;
|
||||
|
||||
@@ -5806,6 +5806,8 @@ export default {
|
||||
autoCloseBracketsDescription: "Automatically insert closing brackets and quotes when typing an opening one",
|
||||
insertSpaceAfterCompletion: "Insert a space after completion",
|
||||
insertSpaceAfterCompletionDescription: "Append a space after accepting a keyword, table, or column completion when the next character allows it",
|
||||
sortCompletionColumnsAlphabetically: "Sort completion columns alphabetically",
|
||||
sortCompletionColumnsAlphabeticallyDescription: "Turn off to keep table fields in their original database order",
|
||||
completionTriggerMode: "Auto-completion trigger mode",
|
||||
completionTriggerModeDescription: "Controls when the SQL completion popup opens automatically. Manual requires a shortcut key; Typed requires a typed identifier prefix; Smart preserves the current behavior.",
|
||||
completionTriggerModeManual: "Manual only",
|
||||
|
||||
@@ -5792,6 +5792,8 @@ export default withEnglishFallback({
|
||||
autoCloseBracketsDescription: "输入左括号或左引号时自动补全对应的右括号或右引号",
|
||||
insertSpaceAfterCompletion: "补全后自动添加空格",
|
||||
insertSpaceAfterCompletionDescription: "选择关键字、表名或列名补全后,在后续字符允许时自动追加空格",
|
||||
sortCompletionColumnsAlphabetically: "按字母顺序排列补全字段",
|
||||
sortCompletionColumnsAlphabeticallyDescription: "关闭后,表字段将保持数据库中的原始顺序",
|
||||
completionTriggerMode: "自动补全触发方式",
|
||||
completionTriggerModeDescription: "控制 SQL 补全弹窗何时自动打开。仅手动提示需快捷键;输入标识符后提示需输入至少一个字符;智能位置提示保持当前行为。",
|
||||
completionTriggerModeManual: "仅手动提示",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { acceptCompletion, autocompletion, completionStatus, currentCompletions,
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { completionLabelPresentation } from "@/lib/editor/sqlCompletionPresentation";
|
||||
import { compareSqlCompletions, completionLabelPresentation } from "@/lib/editor/sqlCompletionPresentation";
|
||||
|
||||
describe("SQL completion label presentation", () => {
|
||||
it("filters a custom snippet by trigger while preserving its display label", () => {
|
||||
@@ -44,3 +44,90 @@ describe("SQL completion label presentation", () => {
|
||||
view.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("SQL completion order", () => {
|
||||
it("keeps the current alphabetical tie-break by default", () => {
|
||||
const items = [
|
||||
{ label: "id", type: "column" },
|
||||
{ label: "z_last", type: "column" },
|
||||
{ label: "a_first", type: "column" },
|
||||
];
|
||||
|
||||
expect([...items].sort((a, b) => compareSqlCompletions(a, b, true)).map((item) => item.label)).toEqual(["a_first", "id", "z_last"]);
|
||||
});
|
||||
|
||||
it("preserves database order for column ties without changing their boosts", () => {
|
||||
const items = [
|
||||
{ label: "id", type: "column", boost: 100 },
|
||||
{ label: "z_last", type: "column", boost: 10 },
|
||||
{ label: "a_first", type: "column", boost: 10 },
|
||||
];
|
||||
|
||||
expect([...items].sort((a, b) => compareSqlCompletions(a, b, false)).map((item) => item.label)).toEqual(["id", "z_last", "a_first"]);
|
||||
expect(items.map((item) => item.boost)).toEqual([100, 10, 10]);
|
||||
});
|
||||
|
||||
it("keeps label ordering for non-columns and mixed item types", () => {
|
||||
expect(compareSqlCompletions({ label: "SELECT", type: "keyword" }, { label: "FROM", type: "keyword" }, false)).toBeGreaterThan(0);
|
||||
expect(compareSqlCompletions({ label: "z_column", type: "column" }, { label: "a_table", type: "table" }, false)).toBeGreaterThan(0);
|
||||
expect(compareSqlCompletions({ label: "visible", sortText: "a_sort", type: "snippet" }, { label: "before", type: "keyword" }, false)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it("preserves database order in CodeMirror when column match scores tie", async () => {
|
||||
const view = new EditorView({
|
||||
parent: document.createElement("div"),
|
||||
state: EditorState.create({
|
||||
extensions: [
|
||||
autocompletion({
|
||||
interactionDelay: 0,
|
||||
compareCompletions: (a, b) => compareSqlCompletions(a, b, false),
|
||||
override: [
|
||||
() => ({
|
||||
from: 0,
|
||||
options: [
|
||||
{ label: "id", type: "column", boost: 100 },
|
||||
{ label: "z_last", type: "column", boost: 10 },
|
||||
{ label: "a_first", type: "column", boost: 10 },
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
startCompletion(view);
|
||||
await expect.poll(() => completionStatus(view.state)).toBe("active");
|
||||
expect(currentCompletions(view.state).map((item) => item.label)).toEqual(["id", "z_last", "a_first"]);
|
||||
view.destroy();
|
||||
});
|
||||
|
||||
it("keeps the default CodeMirror alphabetical tie-break and key priority", async () => {
|
||||
const view = new EditorView({
|
||||
parent: document.createElement("div"),
|
||||
state: EditorState.create({
|
||||
extensions: [
|
||||
autocompletion({
|
||||
interactionDelay: 0,
|
||||
compareCompletions: (a, b) => compareSqlCompletions(a, b, true),
|
||||
override: [
|
||||
() => ({
|
||||
from: 0,
|
||||
options: [
|
||||
{ label: "id", type: "column", boost: 100 },
|
||||
{ label: "z_last", type: "column", boost: 10 },
|
||||
{ label: "a_first", type: "column", boost: 10 },
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
startCompletion(view);
|
||||
await expect.poll(() => completionStatus(view.state)).toBe("active");
|
||||
expect(currentCompletions(view.state).map((item) => item.label)).toEqual(["id", "a_first", "z_last"]);
|
||||
view.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,12 @@ export interface CompletionLabelPresentation {
|
||||
sortText?: string;
|
||||
}
|
||||
|
||||
export interface CompletionSortItem {
|
||||
label: string;
|
||||
sortText?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export function completionLabelPresentation(label: string, filterText?: string): CompletionLabelPresentation {
|
||||
const normalizedFilterText = filterText?.trim();
|
||||
if (!normalizedFilterText || label.toLowerCase().startsWith(normalizedFilterText.toLowerCase())) return { label };
|
||||
@@ -13,3 +19,8 @@ export function completionLabelPresentation(label: string, filterText?: string):
|
||||
sortText: label,
|
||||
};
|
||||
}
|
||||
|
||||
export function compareSqlCompletions(a: CompletionSortItem, b: CompletionSortItem, sortColumnsAlphabetically: boolean): number {
|
||||
if (!sortColumnsAlphabetically && a.type === "column" && b.type === "column") return 0;
|
||||
return (a.sortText || a.label).localeCompare(b.sortText || b.label);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
||||
"showInsertValueHints",
|
||||
"autoAliasTables",
|
||||
"insertSpaceAfterCompletion",
|
||||
"sortCompletionColumnsAlphabetically",
|
||||
"wordWrap",
|
||||
"vimModeEnabled",
|
||||
"autoCloseBrackets",
|
||||
|
||||
@@ -515,6 +515,7 @@ export interface EditorSettings {
|
||||
showInsertValueHints: boolean;
|
||||
autoAliasTables: boolean;
|
||||
insertSpaceAfterCompletion: boolean;
|
||||
sortCompletionColumnsAlphabetically: boolean;
|
||||
wordWrap: boolean;
|
||||
vimModeEnabled: boolean;
|
||||
autoCloseBrackets: boolean;
|
||||
@@ -710,6 +711,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
||||
showInsertValueHints: true,
|
||||
autoAliasTables: true,
|
||||
insertSpaceAfterCompletion: true,
|
||||
sortCompletionColumnsAlphabetically: true,
|
||||
wordWrap: false,
|
||||
vimModeEnabled: false,
|
||||
autoCloseBrackets: true,
|
||||
@@ -1049,6 +1051,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
||||
showInsertValueHints: typeof settings.showInsertValueHints === "boolean" ? settings.showInsertValueHints : DEFAULT_EDITOR_SETTINGS.showInsertValueHints,
|
||||
autoAliasTables: settings.autoAliasTables ?? DEFAULT_EDITOR_SETTINGS.autoAliasTables,
|
||||
insertSpaceAfterCompletion: typeof settings.insertSpaceAfterCompletion === "boolean" ? settings.insertSpaceAfterCompletion : DEFAULT_EDITOR_SETTINGS.insertSpaceAfterCompletion,
|
||||
sortCompletionColumnsAlphabetically: typeof settings.sortCompletionColumnsAlphabetically === "boolean" ? settings.sortCompletionColumnsAlphabetically : DEFAULT_EDITOR_SETTINGS.sortCompletionColumnsAlphabetically,
|
||||
wordWrap: settings.wordWrap ?? DEFAULT_EDITOR_SETTINGS.wordWrap,
|
||||
vimModeEnabled: typeof settings.vimModeEnabled === "boolean" ? settings.vimModeEnabled : DEFAULT_EDITOR_SETTINGS.vimModeEnabled,
|
||||
autoCloseBrackets: typeof settings.autoCloseBrackets === "boolean" ? settings.autoCloseBrackets : DEFAULT_EDITOR_SETTINGS.autoCloseBrackets,
|
||||
@@ -1621,6 +1624,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
if (partial.showInsertValueHints !== undefined) editorSettings.value.showInsertValueHints = partial.showInsertValueHints === true;
|
||||
if (partial.autoAliasTables !== undefined) editorSettings.value.autoAliasTables = partial.autoAliasTables;
|
||||
if (partial.insertSpaceAfterCompletion !== undefined) editorSettings.value.insertSpaceAfterCompletion = partial.insertSpaceAfterCompletion === true;
|
||||
if (partial.sortCompletionColumnsAlphabetically !== undefined) editorSettings.value.sortCompletionColumnsAlphabetically = partial.sortCompletionColumnsAlphabetically === true;
|
||||
if (partial.wordWrap !== undefined) editorSettings.value.wordWrap = partial.wordWrap;
|
||||
if (partial.vimModeEnabled !== undefined) editorSettings.value.vimModeEnabled = partial.vimModeEnabled === true;
|
||||
if (partial.autoCloseBrackets !== undefined) editorSettings.value.autoCloseBrackets = partial.autoCloseBrackets === true;
|
||||
|
||||
@@ -40,3 +40,11 @@ test("applies a result grid font without overwriting editor or interface fonts",
|
||||
assert.equal(patch.tableFontFamily, tableFontFamily);
|
||||
assert.equal(store.editorSettings.tableFontFamily, tableFontFamily);
|
||||
});
|
||||
|
||||
test("round-trips completion column ordering through the editor settings draft", () => {
|
||||
const base = editorSettingsDraftFromSettings(DEFAULT_EDITOR_SETTINGS);
|
||||
const patch = editorSettingsPatchFromDraft({ ...base, sortCompletionColumnsAlphabetically: false }, base);
|
||||
|
||||
assert.deepEqual(patch, { sortCompletionColumnsAlphabetically: false });
|
||||
assert.equal(editorSettingsDraftFromSettings({ ...DEFAULT_EDITOR_SETTINGS, ...patch }).sortCompletionColumnsAlphabetically, false);
|
||||
});
|
||||
|
||||
+23
@@ -99,6 +99,29 @@ test("numericColumnRightAlign defaults to true and round-trips through normalize
|
||||
assert.equal(normalizeEditorSettings({ numericColumnRightAlign: "false" as unknown as boolean }).numericColumnRightAlign, true);
|
||||
});
|
||||
|
||||
test("completion column sorting defaults to alphabetical and normalizes saved booleans", () => {
|
||||
assert.equal(DEFAULT_EDITOR_SETTINGS.sortCompletionColumnsAlphabetically, true);
|
||||
assert.equal(normalizeEditorSettings({}).sortCompletionColumnsAlphabetically, true);
|
||||
assert.equal(normalizeEditorSettings({ sortCompletionColumnsAlphabetically: false }).sortCompletionColumnsAlphabetically, false);
|
||||
assert.equal(normalizeEditorSettings({ sortCompletionColumnsAlphabetically: true }).sortCompletionColumnsAlphabetically, true);
|
||||
assert.equal(normalizeEditorSettings({ sortCompletionColumnsAlphabetically: "false" as unknown as boolean }).sortCompletionColumnsAlphabetically, true);
|
||||
});
|
||||
|
||||
test("updateEditorSettings persists completion column sort toggles", async () => {
|
||||
await withMockLocalStorage({}, async () => {
|
||||
setActivePinia(createPinia());
|
||||
const store = useSettingsStore();
|
||||
await store.initEditorSettings();
|
||||
|
||||
store.updateEditorSettings({ sortCompletionColumnsAlphabetically: false });
|
||||
assert.equal(store.editorSettings.sortCompletionColumnsAlphabetically, false);
|
||||
await vi.waitFor(() => {
|
||||
const saved = saveEditorSettingsMock.mock.calls.at(-1)?.[0] as { sortCompletionColumnsAlphabetically?: boolean } | undefined;
|
||||
assert.equal(saved?.sortCompletionColumnsAlphabetically, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("updateEditorSettings persists numericColumnRightAlign toggles", async () => {
|
||||
await withMockLocalStorage({}, async () => {
|
||||
setActivePinia(createPinia());
|
||||
|
||||
Reference in New Issue
Block a user