mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-02 02:34:42 +08:00
feat(editor): optional space completion confirmation for SQL Server
This commit is contained in:
@@ -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 }),
|
||||
|
||||
@@ -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{'}'}",
|
||||
|
||||
@@ -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{'}'}",
|
||||
|
||||
@@ -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{'}'}",
|
||||
|
||||
@@ -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{'}'}",
|
||||
|
||||
@@ -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{'}'}",
|
||||
|
||||
@@ -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{'}'}",
|
||||
|
||||
@@ -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{'}'}",
|
||||
|
||||
@@ -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{'}'}",
|
||||
|
||||
@@ -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{'}'}",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 |
Reference in New Issue
Block a user