feat(editor): configure SQL completion column ordering

Closes #5955
This commit is contained in:
t8y2 committed 2026-08-13 06:54:51 +00:00
1 parent 31a7609604
commit 18d581137c
10 files changed
+157 -3

No files matched your search

@@ -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",