feat(editor): optional space completion confirmation for SQL Server

This commit is contained in:
zipg
2026-09-18 23:43:58 +08:00
committed by GitHub
parent 54652951d1
commit 9d29f53e37
24 changed files with 202 additions and 2 deletions
@@ -253,6 +253,7 @@ import {
import { applyEditorSettingsDraftToRefs, type EditorSettingsDraftRefMap } from "@/lib/settings/applyEditorSettingsDraft";
import { serializeSettingsTransfer, sortTransferCategories, transferCategoryForKey, type SettingsTransferCategoryId } from "@/lib/settings/settingsTransfer";
import { useConnectionStore } from "@/stores/connectionStore";
import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { useSavedSqlStore } from "@/stores/savedSqlStore";
import { usePromptTemplateStore } from "@/stores/promptTemplateStore";
import { useTunnelProfileStore } from "@/stores/tunnelProfileStore";
@@ -291,6 +292,7 @@ const { t, locale } = useI18n();
const { toast } = useToast();
const settingsStore = useSettingsStore();
const connectionStore = useConnectionStore();
const hasSqlServerConnection = computed(() => connectionStore.connections.some((connection) => effectiveDatabaseTypeForConnection(connection) === "sqlserver"));
const savedSqlStore = useSavedSqlStore();
const promptTemplateStore = usePromptTemplateStore();
const tunnelProfileStore = useTunnelProfileStore();
@@ -568,6 +570,8 @@ 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 editSqlServerSpaceConfirmsCompletion = ref(settingsStore.editorSettings.sqlServerSpaceConfirmsCompletion);
const showSqlServerSpaceConfirmsCompletion = computed(() => hasSqlServerConnection.value || settingsStore.editorSettings.sqlServerSpaceConfirmsCompletion || editSqlServerSpaceConfirmsCompletion.value);
const editSortCompletionColumnsAlphabetically = ref(settingsStore.editorSettings.sortCompletionColumnsAlphabetically);
const editSelectFirstCompletionOnOpen = ref(settingsStore.editorSettings.selectFirstCompletionOnOpen);
const editCompletionTriggerMode = ref<SqlCompletionTriggerMode>(settingsStore.editorSettings.completionTriggerMode);
@@ -904,6 +908,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
showInsertValueHints: editShowInsertValueHints.value,
autoAliasTables: editAutoAliasTables.value,
insertSpaceAfterCompletion: editInsertSpaceAfterCompletion.value,
sqlServerSpaceConfirmsCompletion: editSqlServerSpaceConfirmsCompletion.value,
sortCompletionColumnsAlphabetically: editSortCompletionColumnsAlphabetically.value,
selectFirstCompletionOnOpen: editSelectFirstCompletionOnOpen.value,
completionTriggerMode: editCompletionTriggerMode.value,
@@ -1541,6 +1546,7 @@ function syncEditorSettingsDraftFromStore() {
editShowInsertValueHints.value = settingsStore.editorSettings.showInsertValueHints;
editAutoAliasTables.value = settingsStore.editorSettings.autoAliasTables;
editInsertSpaceAfterCompletion.value = settingsStore.editorSettings.insertSpaceAfterCompletion;
editSqlServerSpaceConfirmsCompletion.value = settingsStore.editorSettings.sqlServerSpaceConfirmsCompletion;
editSortCompletionColumnsAlphabetically.value = settingsStore.editorSettings.sortCompletionColumnsAlphabetically;
editSelectFirstCompletionOnOpen.value = settingsStore.editorSettings.selectFirstCompletionOnOpen;
editCompletionTriggerMode.value = settingsStore.editorSettings.completionTriggerMode;
@@ -1671,6 +1677,7 @@ const editorSettingsDraftRefs: EditorSettingsDraftRefMap = {
showInsertValueHints: editShowInsertValueHints,
autoAliasTables: editAutoAliasTables,
insertSpaceAfterCompletion: editInsertSpaceAfterCompletion,
sqlServerSpaceConfirmsCompletion: editSqlServerSpaceConfirmsCompletion,
sortCompletionColumnsAlphabetically: editSortCompletionColumnsAlphabetically,
selectFirstCompletionOnOpen: editSelectFirstCompletionOnOpen,
wordWrap: editWordWrap,
@@ -2114,6 +2121,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editShowInsertValueHints.value = DEFAULT_EDITOR_SETTINGS.showInsertValueHints;
editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables;
editInsertSpaceAfterCompletion.value = DEFAULT_EDITOR_SETTINGS.insertSpaceAfterCompletion;
editSqlServerSpaceConfirmsCompletion.value = DEFAULT_EDITOR_SETTINGS.sqlServerSpaceConfirmsCompletion;
editSortCompletionColumnsAlphabetically.value = DEFAULT_EDITOR_SETTINGS.sortCompletionColumnsAlphabetically;
editSelectFirstCompletionOnOpen.value = DEFAULT_EDITOR_SETTINGS.selectFirstCompletionOnOpen;
editCompletionTriggerMode.value = DEFAULT_EDITOR_SETTINGS.completionTriggerMode;
@@ -2259,6 +2267,7 @@ function resetAllDefaults() {
editShowInsertValueHints.value = DEFAULT_EDITOR_SETTINGS.showInsertValueHints;
editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables;
editInsertSpaceAfterCompletion.value = DEFAULT_EDITOR_SETTINGS.insertSpaceAfterCompletion;
editSqlServerSpaceConfirmsCompletion.value = DEFAULT_EDITOR_SETTINGS.sqlServerSpaceConfirmsCompletion;
editSortCompletionColumnsAlphabetically.value = DEFAULT_EDITOR_SETTINGS.sortCompletionColumnsAlphabetically;
editSelectFirstCompletionOnOpen.value = DEFAULT_EDITOR_SETTINGS.selectFirstCompletionOnOpen;
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
@@ -2792,6 +2801,8 @@ const settingsSearchEntries = computed(() =>
[...SETTINGS_SEARCH_DEFINITIONS, ...createShortcutSettingsSearchDefinitions(SHORTCUT_DEFINITIONS)],
{
isWeb,
hasSqlServerConnection: hasSqlServerConnection.value,
sqlServerSpaceConfirmsCompletionEnabled: settingsStore.editorSettings.sqlServerSpaceConfirmsCompletion || editSqlServerSpaceConfirmsCompletion.value,
visibleCategories: new Set(settingsCategoryNav.value.map((category) => category.value)),
},
translateWithExecuteShortcut,
@@ -6044,6 +6055,16 @@ onUnmounted(() => {
<Switch id="editor-insert-space-after-completion" v-model="editInsertSpaceAfterCompletion" class="mt-0.5" />
</div>
<div v-if="showSqlServerSpaceConfirmsCompletion" class="settings-item 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-sqlserver-space-confirms-completion">{{ t("settings.sqlServerSpaceConfirmsCompletion") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.sqlServerSpaceConfirmsCompletionDescription") }}
</p>
</div>
<Switch id="editor-sqlserver-space-confirms-completion" v-model="editSqlServerSpaceConfirmsCompletion" class="mt-0.5" />
</div>
<div class="settings-item 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>
@@ -2705,6 +2705,38 @@ function acceptCompletionOrNextSnippetField(view: EditorViewType): boolean {
return codeMirrorNextSnippetField?.(view) ?? false;
}
function acceptSqlServerCompletionOnSpace(view: EditorViewType): boolean {
if (isEditorComposing(view)) return false;
if (props.databaseType !== "sqlserver" || !settingsStore.editorSettings.sqlServerSpaceConfirmsCompletion) return false;
if (codeMirrorCompletionStatus?.(view.state) !== "active") return false;
// A non-empty selection belongs to block editing, not word completion.
if (!view.state.selection.main.empty) return false;
const selected = codeMirrorSelectedCompletion?.(view.state) as QueryCompletionOption | null | undefined;
const completionType = selected?.type;
if (completionType !== "keyword" && completionType !== "table" && completionType !== "column") return false;
// Batch-selection rows own Space for checkbox toggling; this Prec.highest binding outranks their keymap.
if (selected?.dbxBatchColumnSelection || selected?.dbxBatchColumnSelectionAction) return false;
if (!(codeMirrorAcceptCompletion?.(view) ?? false)) return false;
const selection = view.state.selection.main;
if (!selection.empty) return true;
const cursor = selection.head;
const previousCharacter = cursor > 0 ? view.state.sliceDoc(cursor - 1, cursor) : "";
if (/\s/.test(previousCharacter)) return true;
const nextCharacter = view.state.sliceDoc(cursor, cursor + 1);
if (/\s/.test(nextCharacter)) {
view.dispatch({ selection: { anchor: cursor + 1 }, scrollIntoView: true });
} else {
view.dispatch({
changes: { from: cursor, insert: " " },
selection: { anchor: cursor + 1 },
scrollIntoView: true,
});
}
return true;
}
function clearPendingCompletionTab() {
if (pendingCompletionTabTimer === null) return;
clearTimeout(pendingCompletionTabTimer);
@@ -6942,6 +6974,7 @@ onMounted(async () => {
vimModeComp.of(vimModeExtension(initialSettings.vimModeEnabled)),
defaultKeymapComp.of(defaultKeymapExtension()),
keymap.of([...searchKeymapWithoutModD(searchKeymap), ...historyKeymap, ...foldKeymap, ...completionKeymap]),
Prec.highest(keymap.of([{ key: "Space", run: acceptSqlServerCompletionOnSpace }])),
sqlLanguageComp.of(buildSqlLanguageExtension()),
sqlSemanticHighlightComp.of(buildSqlSemanticHighlightExtension()),
tooltips({ parent: tooltipParent }),
+2
View File
@@ -7434,6 +7434,8 @@ export default withEnglishFallback({
sqlVariableSyntax_atSetDescription: "İşlətməzdən əvvəl sətirdaxili dəyişən elanlarını aç və dəyərlərini yerinə qoy.",
autoAliasTables: "Cədvəl ləqəblərini avtomatik əlavə et",
autoAliasTablesDescription: "FROM və ya JOIN daxilində cədvəl tamamlaması seçilərkən order_items AS oi kimi yaradılmış ləqəb daxil et.",
sqlServerSpaceConfirmsCompletion: "Boşluqla tamamlamanı təsdiqlə (SQL Server üslubu)",
sqlServerSpaceConfirmsCompletionDescription: "Tamamlama siyahısı açıqkən boşluq düyməsini basmaq seçilmiş açar sözü, cədvəli və ya sütunu təsdiqləyir və boşluğu saxlayır. Enter və Tab əvvəlki kimi işləyir.",
redisKeyTemplatesSection: "Redis",
redisKeyTemplates: "Açar axtarışı şablonları",
redisKeyTemplatesPlaceholder: "Nümunə:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
+2
View File
@@ -7662,6 +7662,8 @@ export default {
sqlVariableSyntax_atSetDescription: "Expand inline variable declarations and substitute their values before running.",
autoAliasTables: "Automatically add table aliases",
autoAliasTablesDescription: "When selecting a table completion in FROM or JOIN, insert a generated alias such as order_items AS oi.",
sqlServerSpaceConfirmsCompletion: "Space confirms completion (SQL Server style)",
sqlServerSpaceConfirmsCompletionDescription: "While the completion list is open, pressing Space accepts the selected keyword, table, or column and keeps the trailing space. Enter and Tab behave as before.",
redisKeyTemplatesSection: "Redis",
redisKeyTemplates: "Key search templates",
redisKeyTemplatesPlaceholder: "Example:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
+2
View File
@@ -7174,6 +7174,8 @@ export default withEnglishFallback({
sqlVariableSyntax_atSetDescription: "Expande las declaraciones de variables en línea y sustituye sus valores antes de ejecutar.",
autoAliasTables: "Agregar alias de tabla automáticamente",
autoAliasTablesDescription: "Al seleccionar una tabla en FROM o JOIN, inserta un alias generado como order_items AS oi.",
sqlServerSpaceConfirmsCompletion: "Confirmar la sugerencia con espacio (estilo SQL Server)",
sqlServerSpaceConfirmsCompletionDescription: "Con la lista de sugerencias abierta, pulsar espacio acepta la palabra clave, tabla o columna seleccionada y conserva el espacio. Intro y Tabulador no cambian.",
redisKeyTemplatesSection: "Redis",
redisKeyTemplates: "Plantillas de búsqueda de claves",
redisKeyTemplatesPlaceholder: "Example:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
+2
View File
@@ -7174,6 +7174,8 @@ export default withEnglishFallback({
sqlVariableSyntax_atSetDescription: "Espande le dichiarazioni di variabili inline e ne sostituisce i valori prima dell'esecuzione.",
autoAliasTables: "Aggiungi alias tabella automaticamente",
autoAliasTablesDescription: "Quando scegli una tabella in FROM o JOIN, inserisce un alias generato come order_items AS oi.",
sqlServerSpaceConfirmsCompletion: "Conferma il completamento con lo spazio (stile SQL Server)",
sqlServerSpaceConfirmsCompletionDescription: "Con l'elenco dei suggerimenti aperto, premendo spazio si accetta la parola chiave, la tabella o la colonna selezionata mantenendo lo spazio. Invio e Tab restano invariati.",
redisKeyTemplatesSection: "Redis",
redisKeyTemplates: "Modelli di ricerca chiavi",
redisKeyTemplatesPlaceholder: "Example:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
+2
View File
@@ -7187,6 +7187,8 @@ export default withEnglishFallback({
sqlVariableSyntax_atSetDescription: "実行前にインライン変数宣言を展開し、その値をインライン化します。",
autoAliasTables: "テーブル別名を自動追加",
autoAliasTablesDescription: "FROM または JOIN でテーブル補完を選択すると、order_items AS oi のような生成済み別名を挿入します。",
sqlServerSpaceConfirmsCompletion: "スペースで補完を確定(SQL Server 方式)",
sqlServerSpaceConfirmsCompletionDescription: "補完リストの表示中にスペースを押すと、選択中のキーワード・テーブル・列を確定し、空白を保持します。Enter と Tab は従来どおり動作します。",
redisKeyTemplatesSection: "Redis",
redisKeyTemplates: "キー検索テンプレート",
redisKeyTemplatesPlaceholder: "Example:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
+2
View File
@@ -6940,6 +6940,8 @@ export default withEnglishFallback({
sqlVariableSyntax_atSetDescription: "실행 전에 인라인 변수 선언을 확장하고 값을 치환합니다.",
autoAliasTables: "자동으로 테이블 별칭 추가",
autoAliasTablesDescription: "FROM이나 JOIN에서 테이블 완성을 선택할 때 order_items AS oi 같은 생성된 별칭을 삽입합니다.",
sqlServerSpaceConfirmsCompletion: "공백으로 완성 확정(SQL Server 방식)",
sqlServerSpaceConfirmsCompletionDescription: "완성 목록이 열려 있을 때 공백을 누르면 선택한 키워드, 테이블 또는 컬럼을 확정하고 공백을 유지합니다. Enter와 Tab은 기존과 동일하게 동작합니다.",
redisKeyTemplatesSection: "Redis",
redisKeyTemplates: "키 검색 템플릿",
redisKeyTemplatesPlaceholder: "Example:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
+2
View File
@@ -7176,6 +7176,8 @@ export default withEnglishFallback({
sqlVariableSyntax_atSetDescription: "Expande as declarações de variáveis inline e substitui seus valores antes de executar.",
autoAliasTables: "Adicionar alias de tabela automaticamente",
autoAliasTablesDescription: "Ao escolher uma tabela em FROM ou JOIN, insere um alias gerado como order_items AS oi.",
sqlServerSpaceConfirmsCompletion: "Confirmar a conclusão com espaço (estilo SQL Server)",
sqlServerSpaceConfirmsCompletionDescription: "Com a lista de sugestões aberta, pressionar espaço aceita a palavra-chave, tabela ou coluna selecionada e mantém o espaço. Enter e Tab continuam iguais.",
redisKeyTemplatesSection: "Redis",
redisKeyTemplates: "Modelos de busca de chave",
redisKeyTemplatesPlaceholder: "Example:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
+2
View File
@@ -7315,6 +7315,8 @@ export default withEnglishFallback({
sqlVariableSyntax_atSetDescription: "Satır içi değişken bildirimlerini genişlet ve çalıştırmadan önce değerlerini yerine koy.",
autoAliasTables: "Tablo takma adlarını otomatik ekle",
autoAliasTablesDescription: "FROM veya JOIN içinde bir tablo tamamlaması seçildiğinde order_items AS oi gibi üretilmiş bir takma ad ekle.",
sqlServerSpaceConfirmsCompletion: "Boşlukla tamamlamayı onayla (SQL Server tarzı)",
sqlServerSpaceConfirmsCompletionDescription: "Tamamlama listesi açıkken boşluk tuşuna basmak seçili anahtar sözcüğü, tabloyu veya sütunu onaylar ve boşluğu korur. Enter ve Tab aynı kalır.",
redisKeyTemplatesSection: "Redis",
redisKeyTemplates: "Anahtar arama şablonları",
redisKeyTemplatesPlaceholder: "Örnek:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
+2
View File
@@ -7634,6 +7634,8 @@ export default withEnglishFallback({
sqlVariableSyntax_atSetDescription: "执行前展开内联变量声明并就地代入其值。",
autoAliasTables: "自动添加表别名",
autoAliasTablesDescription: "补全 FROM 或 JOIN 表名时自动添加别名。",
sqlServerSpaceConfirmsCompletion: "空格确认补全(SQL Server 习惯)",
sqlServerSpaceConfirmsCompletionDescription: "补全列表打开时,按空格直接采用选中的关键字、表名或列名,并保留空格。Enter 和 Tab 保持原有行为。",
redisKeyTemplatesSection: "Redis",
redisKeyTemplates: "Key 搜索模板",
redisKeyTemplatesPlaceholder: "例如:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
+2
View File
@@ -6499,6 +6499,8 @@ export default withEnglishFallback({
sqlVariableSyntax_atSetDescription: "執行前展開內聯變數宣告並就地代入其值。",
autoAliasTables: "自動加入資料表別名",
autoAliasTablesDescription: "在 FROM 或 JOIN 中選擇資料表補全時,自動插入類似 order_items AS oi 的資料表別名。",
sqlServerSpaceConfirmsCompletion: "空格確認補全(SQL Server 習慣)",
sqlServerSpaceConfirmsCompletionDescription: "補全清單開啟時,按空格直接採用選取的關鍵字、資料表或欄名,並保留空格。Enter 與 Tab 維持原本行為。",
redisKeyTemplatesSection: "Redis",
redisKeyTemplates: "Key 搜尋模板",
redisKeyTemplatesPlaceholder: "例如:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
@@ -37,6 +37,7 @@ interface MockState {
doc: {
lineAt: (position: number) => { from: number; text: string };
};
sliceDoc: (from: number, to: number) => string;
selection: { main: MockSelection; ranges: MockSelection[] };
replaceSelection: ReturnType<typeof vi.fn>;
update: ReturnType<typeof vi.fn>;
@@ -52,12 +53,15 @@ interface TabHarness {
handleEnter: (view: MockView) => boolean;
insertNewlineWithoutCompletion: (view: MockView) => boolean;
acceptCompletionOrNextSnippetField: (view: MockView) => boolean;
acceptSqlServerCompletionOnSpace: (view: MockView) => boolean;
clearPendingCompletionTab: () => void;
consumeSqlCompletionAutoStartSuppression: () => boolean;
}
function createHarness(options: {
databaseType?: string;
completionStatus: (state: MockState) => "active" | "pending" | null;
selectedCompletion?: (state: MockState) => { type?: string } | null;
acceptCompletion?: (view: MockView) => boolean;
selectedCompletionIndex?: (state: MockState) => number | null;
selectFirstCompletion?: (view: MockView) => boolean;
@@ -68,6 +72,7 @@ function createHarness(options: {
indentMore?: (view: MockView) => boolean;
acceptCompletionShortcut?: string;
selectFirstCompletionOnOpen?: boolean;
sqlServerSpaceConfirmsCompletion?: boolean;
imeComposing?: (view: MockView) => boolean;
}): TabHarness {
const source = [
@@ -88,6 +93,7 @@ function createHarness(options: {
extractFunction("clearPendingCompletionEnter"),
extractFunction("insertNewlineWithoutCompletion"),
extractFunction("acceptCompletionOrNextSnippetField"),
extractFunction("acceptSqlServerCompletionOnSpace"),
extractFunction("clearPendingCompletionTab"),
extractFunction("waitForCompletionTab"),
extractFunction("consumeSqlCompletionAutoStartSuppression"),
@@ -97,6 +103,7 @@ function createHarness(options: {
}).outputText;
const factory = new Function(
"codeMirrorCompletionStatus",
"codeMirrorSelectedCompletion",
"isBatchColumnSelectionCompletionActive",
"codeMirrorAcceptCompletion",
"codeMirrorSelectedCompletionIndex",
@@ -114,10 +121,11 @@ function createHarness(options: {
"shortcutToCodeMirrorKey",
"props",
"isEditorComposing",
`${javascript}\nreturn { handleTab, handleEnter, insertNewlineWithoutCompletion, acceptCompletionOrNextSnippetField, clearPendingCompletionTab, consumeSqlCompletionAutoStartSuppression };`,
`${javascript}\nreturn { handleTab, handleEnter, insertNewlineWithoutCompletion, acceptCompletionOrNextSnippetField, acceptSqlServerCompletionOnSpace, clearPendingCompletionTab, consumeSqlCompletionAutoStartSuppression };`,
);
return factory(
options.completionStatus,
options.selectedCompletion ?? (() => ({ type: "column" })),
(status: "active" | "pending" | null) => status === "active",
options.acceptCompletion ?? (() => false),
options.selectedCompletionIndex ?? (() => 0),
@@ -135,11 +143,12 @@ function createHarness(options: {
sqlFormatter: { useTabs: false, tabWidth: 2 },
shortcuts: { ...DEFAULT_SHORTCUT_SETTINGS, acceptCompletion: options.acceptCompletionShortcut ?? DEFAULT_SHORTCUT_SETTINGS.acceptCompletion },
selectFirstCompletionOnOpen: options.selectFirstCompletionOnOpen ?? false,
sqlServerSpaceConfirmsCompletion: options.sqlServerSpaceConfirmsCompletion ?? true,
},
},
normalizeShortcutSettings,
shortcutToCodeMirrorKey,
{ databaseType: "mysql" },
{ databaseType: options.databaseType ?? "mysql" },
options.imeComposing ?? ((view: MockView) => (view as MockView & { composing?: boolean }).composing === true || (view as MockView & { compositionStarted?: boolean }).compositionStarted === true),
) as TabHarness;
}
@@ -150,6 +159,7 @@ function createView(text = "SELECT", position = text.length, selectionOverrides:
doc: {
lineAt: () => ({ from: 0, text }),
},
sliceDoc: (from, to) => text.slice(from, to),
selection: { main: selection, ranges: [selection, ...additionalRanges] },
replaceSelection: vi.fn((insert: string) => ({ insert })),
update: vi.fn((change: unknown, options: unknown) => ({ change, options })),
@@ -560,3 +570,77 @@ describe("QueryEditor completion Tab keymap", () => {
expect(startCompletion).not.toHaveBeenCalled();
});
});
describe("QueryEditor SQL Server completion Space keymap", () => {
it.each(["keyword", "table", "column"])("accepts an active %s completion", (type) => {
const acceptCompletion = vi.fn(() => true);
const harness = createHarness({
databaseType: "sqlserver",
completionStatus: () => "active",
selectedCompletion: () => ({ type }),
acceptCompletion,
});
const view = createView("FXXX ");
expect(harness.acceptSqlServerCompletionOnSpace(view)).toBe(true);
expect(acceptCompletion).toHaveBeenCalledWith(view);
expect(view.dispatch).not.toHaveBeenCalled();
});
it("inserts the typed space when automatic completion spacing is disabled", () => {
const harness = createHarness({ databaseType: "sqlserver", completionStatus: () => "active", acceptCompletion: () => true });
const view = createView("FXXX");
expect(harness.acceptSqlServerCompletionOnSpace(view)).toBe(true);
expect(view.dispatch).toHaveBeenCalledWith({
changes: { from: 4, insert: " " },
selection: { anchor: 5 },
scrollIntoView: true,
});
});
it("moves over an existing following space instead of duplicating it", () => {
const harness = createHarness({ databaseType: "sqlserver", completionStatus: () => "active", acceptCompletion: () => true });
const view = createView("FXXX ", 4);
expect(harness.acceptSqlServerCompletionOnSpace(view)).toBe(true);
expect(view.dispatch).toHaveBeenCalledWith({ selection: { anchor: 5 }, scrollIntoView: true });
});
it.each([
{ databaseType: "postgresql", status: "active", completionType: "column" },
{ databaseType: "sqlserver", status: "pending", completionType: "column" },
{ databaseType: "sqlserver", status: "active", completionType: "function" },
{ databaseType: "sqlserver", status: "active", completionType: "snippet" },
])("keeps ordinary Space input for $databaseType/$status/$completionType", ({ databaseType, status, completionType }) => {
const acceptCompletion = vi.fn(() => true);
const harness = createHarness({
databaseType,
completionStatus: () => status as "active" | "pending",
selectedCompletion: () => ({ type: completionType }),
acceptCompletion,
});
expect(harness.acceptSqlServerCompletionOnSpace(createView())).toBe(false);
expect(acceptCompletion).not.toHaveBeenCalled();
});
it("keeps ordinary Space input when CodeMirror cannot accept the selected completion", () => {
const harness = createHarness({ databaseType: "sqlserver", completionStatus: () => "active", acceptCompletion: () => false });
expect(harness.acceptSqlServerCompletionOnSpace(createView())).toBe(false);
});
it("keeps ordinary Space input while the SQL Server space-confirm setting is disabled", () => {
const acceptCompletion = vi.fn(() => true);
const harness = createHarness({
databaseType: "sqlserver",
completionStatus: () => "active",
acceptCompletion,
sqlServerSpaceConfirmsCompletion: false,
});
expect(harness.acceptSqlServerCompletionOnSpace(createView())).toBe(false);
expect(acceptCompletion).not.toHaveBeenCalled();
});
});
@@ -72,6 +72,32 @@ describe("settings search", () => {
expect(searchSettings(entries, "line number", "en").map((entry) => entry.id)).toEqual(["editor-line-numbers"]);
});
it("indexes SQL Server space confirmation when SQL Server exists or an exported setting is enabled", () => {
const definition = SETTINGS_SEARCH_DEFINITIONS.find((entry) => entry.id === "editor-sqlserver-space-completion");
expect(definition).toEqual(
expect.objectContaining({
id: "editor-sqlserver-space-completion",
category: "editor",
titleKey: "settings.sqlServerSpaceConfirmsCompletion",
descriptionKey: "settings.sqlServerSpaceConfirmsCompletionDescription",
targetId: "editor",
visible: expect.any(Function),
}),
);
const context = { isWeb: false, visibleCategories: new Set<SettingsCategory>(["editor"]) };
const withoutSqlServer = resolveSettingsSearchEntries(SETTINGS_SEARCH_DEFINITIONS, { ...context, hasSqlServerConnection: false, sqlServerSpaceConfirmsCompletionEnabled: false }, translate, categoryLabels);
const importedWithoutSqlServer = resolveSettingsSearchEntries(SETTINGS_SEARCH_DEFINITIONS, { ...context, hasSqlServerConnection: false, sqlServerSpaceConfirmsCompletionEnabled: true }, translate, categoryLabels);
const withSqlServer = resolveSettingsSearchEntries(SETTINGS_SEARCH_DEFINITIONS, { ...context, hasSqlServerConnection: true, sqlServerSpaceConfirmsCompletionEnabled: false }, translate, categoryLabels);
expect(withoutSqlServer.map((entry) => entry.id)).not.toContain("editor-sqlserver-space-completion");
expect(importedWithoutSqlServer.map((entry) => entry.id)).toContain("editor-sqlserver-space-completion");
expect(withSqlServer.map((entry) => entry.id)).toContain("editor-sqlserver-space-completion");
expect(settingsDialogSource).toContain('const hasSqlServerConnection = computed(() => connectionStore.connections.some((connection) => effectiveDatabaseTypeForConnection(connection) === "sqlserver"));');
expect(settingsDialogSource).toContain("const showSqlServerSpaceConfirmsCompletion = computed(");
expect(settingsDialogSource).toContain('<div v-if="showSqlServerSpaceConfirmsCompletion" class="settings-item flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">');
});
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");
@@ -26,6 +26,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"showInsertValueHints",
"autoAliasTables",
"insertSpaceAfterCompletion",
"sqlServerSpaceConfirmsCompletion",
"sortCompletionColumnsAlphabetically",
"selectFirstCompletionOnOpen",
"wordWrap",
@@ -13,6 +13,8 @@ export function resolveSettingsCategory(initialTab?: string): SettingsCategory {
export interface SettingsSearchContext {
isWeb: boolean;
hasSqlServerConnection?: boolean;
sqlServerSpaceConfirmsCompletionEnabled?: boolean;
visibleCategories: ReadonlySet<SettingsCategory>;
}
@@ -88,6 +90,7 @@ export function createToolbarVisibilitySettingsSearchDefinitions(items: readonly
const desktopOnly = (context: SettingsSearchContext) => !context.isWeb;
const webOnly = (context: SettingsSearchContext) => context.isWeb;
const sqlServerSpaceCompletionVisible = (context: SettingsSearchContext) => context.hasSqlServerConnection === true || context.sqlServerSpaceConfirmsCompletionEnabled === true;
export interface ShortcutSearchDefinitionSource {
id: string;
@@ -129,6 +132,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
{ id: "editor-vim", category: "editor", titleKey: "settings.vimMode", descriptionKey: "settings.vimModeDescription", targetId: "editor" },
{ id: "editor-brackets", category: "editor", titleKey: "settings.autoCloseBrackets", descriptionKey: "settings.autoCloseBracketsDescription", targetId: "editor" },
{ id: "editor-completion-spacing", category: "editor", titleKey: "settings.insertSpaceAfterCompletion", descriptionKey: "settings.insertSpaceAfterCompletionDescription", targetId: "editor" },
{ id: "editor-sqlserver-space-completion", category: "editor", titleKey: "settings.sqlServerSpaceConfirmsCompletion", descriptionKey: "settings.sqlServerSpaceConfirmsCompletionDescription", targetId: "editor", visible: sqlServerSpaceCompletionVisible },
{ id: "editor-completion-trigger-mode", category: "editor", titleKey: "settings.completionTriggerMode", descriptionKey: "settings.completionTriggerModeDescription", targetId: "editor" },
{ id: "editor-auto-alias", category: "editor", titleKey: "settings.autoAliasTables", descriptionKey: "settings.autoAliasTablesDescription", targetId: "editor" },
{ id: "editor-unsaved-close", category: "editor", titleKey: "settings.confirmUnsavedSqlClose", descriptionKey: "settings.confirmUnsavedSqlCloseDescription", targetId: "editor" },
@@ -83,6 +83,7 @@ const SETTINGS_TRANSFER_CATEGORY_KEYS: Record<SettingsTransferCategoryId, readon
"showInsertValueHints",
"autoAliasTables",
"insertSpaceAfterCompletion",
"sqlServerSpaceConfirmsCompletion",
"sortCompletionColumnsAlphabetically",
"selectFirstCompletionOnOpen",
"completionTriggerMode",
@@ -211,6 +211,12 @@ describe("normalizeEditorSettings", () => {
expect(normalizeEditorSettings({ insertSpaceAfterCompletion: false }).insertSpaceAfterCompletion).toBe(false);
});
it("keeps SQL Server space-confirm completion off by default and preserves an explicit opt-in", () => {
expect(normalizeEditorSettings({}).sqlServerSpaceConfirmsCompletion).toBe(false);
expect(normalizeEditorSettings({ sqlServerSpaceConfirmsCompletion: true }).sqlServerSpaceConfirmsCompletion).toBe(true);
expect(normalizeEditorSettings({ sqlServerSpaceConfirmsCompletion: "yes" as unknown as boolean }).sqlServerSpaceConfirmsCompletion).toBe(false);
});
it("selects the first completion candidate by default and preserves the opt-out", () => {
expect(normalizeEditorSettings({}).selectFirstCompletionOnOpen).toBe(true);
expect(normalizeEditorSettings({ selectFirstCompletionOnOpen: true }).selectFirstCompletionOnOpen).toBe(true);
+4
View File
@@ -771,6 +771,7 @@ export interface EditorSettings {
showInsertValueHints: boolean;
autoAliasTables: boolean;
insertSpaceAfterCompletion: boolean;
sqlServerSpaceConfirmsCompletion: boolean;
sortCompletionColumnsAlphabetically: boolean;
selectFirstCompletionOnOpen: boolean;
wordWrap: boolean;
@@ -1028,6 +1029,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
showInsertValueHints: true,
autoAliasTables: true,
insertSpaceAfterCompletion: true,
sqlServerSpaceConfirmsCompletion: false,
sortCompletionColumnsAlphabetically: true,
selectFirstCompletionOnOpen: true,
wordWrap: false,
@@ -1532,6 +1534,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,
sqlServerSpaceConfirmsCompletion: typeof settings.sqlServerSpaceConfirmsCompletion === "boolean" ? settings.sqlServerSpaceConfirmsCompletion : DEFAULT_EDITOR_SETTINGS.sqlServerSpaceConfirmsCompletion,
sortCompletionColumnsAlphabetically: typeof settings.sortCompletionColumnsAlphabetically === "boolean" ? settings.sortCompletionColumnsAlphabetically : DEFAULT_EDITOR_SETTINGS.sortCompletionColumnsAlphabetically,
selectFirstCompletionOnOpen: typeof settings.selectFirstCompletionOnOpen === "boolean" ? settings.selectFirstCompletionOnOpen : DEFAULT_EDITOR_SETTINGS.selectFirstCompletionOnOpen,
wordWrap: settings.wordWrap ?? DEFAULT_EDITOR_SETTINGS.wordWrap,
@@ -2308,6 +2311,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.sqlServerSpaceConfirmsCompletion !== undefined) editorSettings.value.sqlServerSpaceConfirmsCompletion = partial.sqlServerSpaceConfirmsCompletion === true;
if (partial.sortCompletionColumnsAlphabetically !== undefined) editorSettings.value.sortCompletionColumnsAlphabetically = partial.sortCompletionColumnsAlphabetically === true;
if (partial.selectFirstCompletionOnOpen !== undefined) editorSettings.value.selectFirstCompletionOnOpen = partial.selectFirstCompletionOnOpen === true;
if (partial.wordWrap !== undefined) editorSettings.value.wordWrap = partial.wordWrap;
Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB