fix(editor): restore configurable completion acceptance

This commit is contained in:
zipg authored and GitHub committed 2026-08-25 21:52:01 +08:00
1 parent f9c4b19a60
commit c6e9e94c58
17 files changed
+367 -73

No files matched your search

@@ -332,6 +332,7 @@ const editShowInsertValueHints = ref(settingsStore.editorSettings.showInsertValu
const editAutoAliasTables = ref(settingsStore.editorSettings.autoAliasTables);
const editInsertSpaceAfterCompletion = ref(settingsStore.editorSettings.insertSpaceAfterCompletion);
const editSortCompletionColumnsAlphabetically = ref(settingsStore.editorSettings.sortCompletionColumnsAlphabetically);
const editSelectFirstCompletionOnOpen = ref(settingsStore.editorSettings.selectFirstCompletionOnOpen);
const editCompletionTriggerMode = ref<SqlCompletionTriggerMode>(settingsStore.editorSettings.completionTriggerMode);
const editWordWrap = ref(settingsStore.editorSettings.wordWrap);
const editVimModeEnabled = ref(settingsStore.editorSettings.vimModeEnabled);
@@ -536,6 +537,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
autoAliasTables: editAutoAliasTables.value,
insertSpaceAfterCompletion: editInsertSpaceAfterCompletion.value,
sortCompletionColumnsAlphabetically: editSortCompletionColumnsAlphabetically.value,
selectFirstCompletionOnOpen: editSelectFirstCompletionOnOpen.value,
completionTriggerMode: editCompletionTriggerMode.value,
wordWrap: editWordWrap.value,
vimModeEnabled: editVimModeEnabled.value,
@@ -825,6 +827,7 @@ function syncEditorSettingsDraftFromStore() {
editAutoAliasTables.value = settingsStore.editorSettings.autoAliasTables;
editInsertSpaceAfterCompletion.value = settingsStore.editorSettings.insertSpaceAfterCompletion;
editSortCompletionColumnsAlphabetically.value = settingsStore.editorSettings.sortCompletionColumnsAlphabetically;
editSelectFirstCompletionOnOpen.value = settingsStore.editorSettings.selectFirstCompletionOnOpen;
editCompletionTriggerMode.value = settingsStore.editorSettings.completionTriggerMode;
editWordWrap.value = settingsStore.editorSettings.wordWrap;
editVimModeEnabled.value = settingsStore.editorSettings.vimModeEnabled;
@@ -1079,6 +1082,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables;
editInsertSpaceAfterCompletion.value = DEFAULT_EDITOR_SETTINGS.insertSpaceAfterCompletion;
editSortCompletionColumnsAlphabetically.value = DEFAULT_EDITOR_SETTINGS.sortCompletionColumnsAlphabetically;
editSelectFirstCompletionOnOpen.value = DEFAULT_EDITOR_SETTINGS.selectFirstCompletionOnOpen;
editCompletionTriggerMode.value = DEFAULT_EDITOR_SETTINGS.completionTriggerMode;
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
editVimModeEnabled.value = DEFAULT_EDITOR_SETTINGS.vimModeEnabled;
@@ -1194,6 +1198,7 @@ function resetAllDefaults() {
editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables;
editInsertSpaceAfterCompletion.value = DEFAULT_EDITOR_SETTINGS.insertSpaceAfterCompletion;
editSortCompletionColumnsAlphabetically.value = DEFAULT_EDITOR_SETTINGS.sortCompletionColumnsAlphabetically;
editSelectFirstCompletionOnOpen.value = DEFAULT_EDITOR_SETTINGS.selectFirstCompletionOnOpen;
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
editVimModeEnabled.value = DEFAULT_EDITOR_SETTINGS.vimModeEnabled;
editAutoCloseBrackets.value = DEFAULT_EDITOR_SETTINGS.autoCloseBrackets;
@@ -4030,16 +4035,6 @@ onUnmounted(() => {
<Switch id="editor-vim-mode" v-model="editVimModeEnabled" 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-close-brackets">{{ t("settings.autoCloseBrackets") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.autoCloseBracketsDescription") }}
</p>
</div>
<Switch id="editor-auto-close-brackets" v-model="editAutoCloseBrackets" 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="regex-max-match-count">{{ t("settings.regexMaxMatchCount") }}</Label>
@@ -4055,55 +4050,87 @@ onUnmounted(() => {
class="h-7 w-24 px-2 text-xs tabular-nums [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
/>
</div>
</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-insert-space-after-completion">{{ t("settings.insertSpaceAfterCompletion") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.insertSpaceAfterCompletionDescription") }}
</p>
</div>
<Switch id="editor-insert-space-after-completion" v-model="editInsertSpaceAfterCompletion" class="mt-0.5" />
<Separator />
<div class="space-y-3">
<div class="text-sm font-medium text-muted-foreground">
{{ t("settings.sqlCompletionSection") }}
</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>
<div class="grid gap-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
<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-close-brackets">{{ t("settings.autoCloseBrackets") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.autoCloseBracketsDescription") }}
</p>
</div>
<Switch id="editor-auto-close-brackets" v-model="editAutoCloseBrackets" 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-insert-space-after-completion">{{ t("settings.insertSpaceAfterCompletion") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.insertSpaceAfterCompletionDescription") }}
</p>
</div>
<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-select-first-completion-on-open">{{ t("settings.selectFirstCompletionOnOpen") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.selectFirstCompletionOnOpenDescription") }}
</p>
</div>
<Switch id="editor-select-first-completion-on-open" v-model="editSelectFirstCompletionOnOpen" 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>
<p class="text-xs text-muted-foreground">
{{ t("settings.autoAliasTablesDescription") }}
</p>
</div>
<Switch id="editor-auto-alias-tables" v-model="editAutoAliasTables" class="mt-0.5" />
</div>
<div class="space-y-2">
<Label>{{ t("settings.completionTriggerMode") }}</Label>
<Select :model-value="editCompletionTriggerMode" @update:model-value="onCompletionTriggerModeChange">
<SelectTrigger>
<SelectValue :placeholder="t('settings.completionTriggerMode')" />
</SelectTrigger>
<SelectContent>
<SelectItem value="manual">{{ t("settings.completionTriggerModeManual") }}</SelectItem>
<SelectItem value="require-prefix">{{ t("settings.completionTriggerModeRequirePrefix") }}</SelectItem>
<SelectItem value="positional">{{ t("settings.completionTriggerModePositional") }}</SelectItem>
</SelectContent>
</Select>
<p class="text-xs text-muted-foreground">
{{ t("settings.sortCompletionColumnsAlphabeticallyDescription") }}
{{ t("settings.completionTriggerModeDescription") }}
</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>
<p class="text-xs text-muted-foreground">
{{ t("settings.autoAliasTablesDescription") }}
</p>
</div>
<Switch id="editor-auto-alias-tables" v-model="editAutoAliasTables" class="mt-0.5" />
</div>
<div class="space-y-2">
<Label>{{ t("settings.completionTriggerMode") }}</Label>
<Select :model-value="editCompletionTriggerMode" @update:model-value="onCompletionTriggerModeChange">
<SelectTrigger>
<SelectValue :placeholder="t('settings.completionTriggerMode')" />
</SelectTrigger>
<SelectContent>
<SelectItem value="manual">{{ t("settings.completionTriggerModeManual") }}</SelectItem>
<SelectItem value="require-prefix">{{ t("settings.completionTriggerModeRequirePrefix") }}</SelectItem>
<SelectItem value="positional">{{ t("settings.completionTriggerModePositional") }}</SelectItem>
</SelectContent>
</Select>
<p class="text-xs text-muted-foreground">
{{ t("settings.completionTriggerModeDescription") }}
</p>
</div>
</div>
<Separator />
<div class="grid gap-3 md:grid-cols-2">
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2 md:col-span-2">
<div class="min-w-0 space-y-1">
@@ -131,6 +131,7 @@ import { createShellLineCommentHighlight } from "@/lib/editor/codemirrorShellLin
import { extendQueryEditorSelection, runQueryEditorAltExtendSelection } from "@/lib/editor/queryEditorExtendSelection";
import { createQueryEditorStringMouseSelection } from "@/lib/editor/queryEditorStringMouseSelection";
import { createQueryEditorCompletionShortcutBindings } from "@/lib/editor/queryEditorCompletionShortcut";
import { acceptSelectedCompletionWithRetry, acceptSelectedOrFirstCompletion } from "@/lib/editor/queryEditorCompletionAcceptance";
import type { StatementExecutionMarker } from "@/lib/tabs/tabPresentation";
import { isSchemaAware, isSingleDatabase, supportsDatabaseNameCompletion, supportsDatabaseSchemaQualifier, supportsQueryEditorBlockComments, supportsSqlInListPaste } from "@/lib/database/databaseFeatureSupport";
import { metadataSchemaForConnection, sqlSnippetDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
@@ -202,6 +203,7 @@ const COMPLETION_REMOTE_LATENCY_BUDGET_MS = 120;
const COMPLETION_DEBOUNCE_DELAY_MS = 150;
const COMPLETION_TAB_RETRY_DELAY_MS = 16;
const COMPLETION_TAB_MAX_WAIT_MS = COMPLETION_DEBOUNCE_DELAY_MS + COMPLETION_REMOTE_LATENCY_BUDGET_MS + 100;
const COMPLETION_ENTER_MAX_WAIT_MS = 125;
// Internal rollback switch: flip to false to route completion, diagnostics, and navigation through the legacy SQL context path.
const SEMANTIC_SQL_COMPLETION_ENABLED = true;
@@ -554,6 +556,8 @@ let buildSqlSemanticHighlightExtension: (() => import("@codemirror/state").Exten
let codeMirrorSnippetCompletion: typeof import("@codemirror/autocomplete").snippetCompletion;
let codeMirrorCompletionStatus: typeof import("@codemirror/autocomplete").completionStatus | null = null;
let codeMirrorAcceptCompletion: typeof import("@codemirror/autocomplete").acceptCompletion | null = null;
let codeMirrorSelectedCompletionIndex: typeof import("@codemirror/autocomplete").selectedCompletionIndex | null = null;
let codeMirrorSelectFirstCompletion: import("@codemirror/view").Command | null = null;
let codeMirrorStartCompletion: typeof import("@codemirror/autocomplete").startCompletion | null = null;
let codeMirrorCloseCompletion: typeof import("@codemirror/autocomplete").closeCompletion | null = null;
let codeMirrorInsertCompletionText: typeof import("@codemirror/autocomplete").insertCompletionText | null = null;
@@ -574,6 +578,7 @@ let codeMirrorToggleBlockComment: typeof import("@codemirror/commands").toggleBl
let codeMirrorDefaultKeymap: readonly import("@codemirror/view").KeyBinding[] | null = null;
let codeMirrorToggleFold: typeof import("@codemirror/language").toggleFold | null = null;
let pendingCompletionTabTimer: ReturnType<typeof setTimeout> | null = null;
let cancelPendingCompletionEnter: (() => void) | null = null;
let setSqlDiagnosticsEffect: import("@codemirror/state").StateEffectType<SqlSemanticDiagnostic[]> | null = null;
let setPreviewRangeEffect:
| import("@codemirror/state").StateEffectType<{
@@ -1925,7 +1930,7 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
codeMirrorKeymap.of([
{
key: "Enter",
run: insertNewlineWithoutCompletion,
run: handleEnter,
},
...binding(shortcuts.find, openSearch),
...binding(shortcuts.replace, openReplace),
@@ -1995,6 +2000,36 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
];
}
function handleEnter(view: EditorViewType): boolean {
clearPendingCompletionEnter();
if (settingsStore.editorSettings.selectFirstCompletionOnOpen && codeMirrorCompletionStatus) {
let cancelRetry: (() => void) | null = null;
const result = acceptSelectedCompletionWithRetry(view, {
completionStatus: codeMirrorCompletionStatus,
acceptCompletion: codeMirrorAcceptCompletion,
selectedCompletionIndex: codeMirrorSelectedCompletionIndex,
selectFirstCompletion: codeMirrorSelectFirstCompletion,
retryDelayMs: COMPLETION_TAB_RETRY_DELAY_MS,
maxWaitMs: COMPLETION_ENTER_MAX_WAIT_MS,
onUnavailable: () => insertNewlineWithoutCompletion(view),
onSettled: () => {
if (cancelPendingCompletionEnter === cancelRetry) cancelPendingCompletionEnter = null;
},
});
if (result.handled) {
cancelRetry = result.cancel ?? null;
cancelPendingCompletionEnter = cancelRetry;
return true;
}
}
return insertNewlineWithoutCompletion(view);
}
function clearPendingCompletionEnter() {
cancelPendingCompletionEnter?.();
cancelPendingCompletionEnter = null;
}
function insertNewlineWithoutCompletion(view: EditorViewType): boolean {
codeMirrorCloseCompletion?.(view);
suppressNextSqlCompletionAutoStartUntil = Date.now() + 750;
@@ -2018,7 +2053,7 @@ function acceptCompletionOrNextSnippetField(view: EditorViewType): boolean {
// of the indent edit itself, so it must never hijack this or a following Tab.
if (view.state.selection.ranges.every((range) => range.empty)) {
const completionStatus = codeMirrorCompletionStatus?.(view.state) ?? null;
if (completionStatus === "active" && (codeMirrorAcceptCompletion?.(view) ?? false)) return true;
if (completionStatus === "active" && acceptSelectedOrFirstCompletion(view, codeMirrorAcceptCompletion, codeMirrorSelectedCompletionIndex, codeMirrorSelectFirstCompletion)) return true;
if (completionStatus) return waitForCompletionTab(view);
}
return codeMirrorNextSnippetField?.(view) ?? false;
@@ -2042,7 +2077,7 @@ function waitForCompletionTab(view: EditorViewType): boolean {
if (view.state.doc !== initialDoc || selectionRanges.length !== initialSelectionRanges.length || selectionRanges.some((range, index) => !range.empty || range.anchor !== initialSelectionRanges[index]?.anchor || range.head !== initialSelectionRanges[index]?.head)) return;
const completionStatus = codeMirrorCompletionStatus?.(view.state) ?? null;
if (completionStatus === "active" && (codeMirrorAcceptCompletion?.(view) ?? false)) return;
if (completionStatus === "active" && acceptSelectedOrFirstCompletion(view, codeMirrorAcceptCompletion, codeMirrorSelectedCompletionIndex, codeMirrorSelectFirstCompletion)) return;
if (completionStatus && Date.now() - startedAt < COMPLETION_TAB_MAX_WAIT_MS) {
pendingCompletionTabTimer = setTimeout(retry, COMPLETION_TAB_RETRY_DELAY_MS);
return;
@@ -4661,7 +4696,7 @@ onMounted(async () => {
},
{ EditorState, EditorSelection, Compartment, Prec, RangeSet, StateEffect, StateField },
langSql,
{ autocompletion, startCompletion, acceptCompletion, closeBrackets, closeBracketsKeymap, snippetCompletion, completionStatus, completionKeymap, insertCompletionText, nextSnippetField, closeCompletion },
{ autocompletion, startCompletion, acceptCompletion, closeBrackets, closeBracketsKeymap, snippetCompletion, completionStatus, completionKeymap, insertCompletionText, nextSnippetField, closeCompletion, moveCompletionSelection, selectedCompletionIndex },
{ copyLineDown, copyLineUp, deleteLine, indentLess, indentMore, insertNewlineKeepIndent, moveLineDown, moveLineUp, redo, selectAll, undo, toggleLineComment, toggleBlockComment, history, defaultKeymap, historyKeymap },
{ bracketMatching, foldGutter, indentOnInput, indentUnit, syntaxHighlighting, defaultHighlightStyle, foldKeymap, toggleFold, ensureSyntaxTree, highlightingFor, syntaxTree },
{ searchKeymap },
@@ -4698,6 +4733,8 @@ onMounted(async () => {
setSqlDiagnosticsEffect = StateEffect.define<SqlSemanticDiagnostic[]>();
codeMirrorCompletionStatus = completionStatus;
codeMirrorAcceptCompletion = acceptCompletion;
codeMirrorSelectedCompletionIndex = selectedCompletionIndex;
codeMirrorSelectFirstCompletion = moveCompletionSelection(true);
codeMirrorCloseCompletion = closeCompletion;
codeMirrorStartCompletion = startCompletion;
codeMirrorInsertCompletionText = insertCompletionText;
@@ -4931,7 +4968,7 @@ onMounted(async () => {
buildSqlCompletionExtension = () =>
autocompletion({
activateOnTyping: true,
selectOnOpen: false,
selectOnOpen: settingsStore.editorSettings.selectFirstCompletionOnOpen,
compareCompletions: (a, b) => compareSqlCompletions(a, b, settingsStore.editorSettings.sortCompletionColumnsAlphabetically),
override: [async (context: CompletionContext) => provideSqlCompletions(context)],
});
@@ -5841,7 +5878,7 @@ watch(
);
watch(
() => [settingsStore.editorSettings.snippets, settingsStore.editorSettings.sortCompletionColumnsAlphabetically],
() => [settingsStore.editorSettings.snippets, settingsStore.editorSettings.sortCompletionColumnsAlphabetically, settingsStore.editorSettings.selectFirstCompletionOnOpen],
() => {
completionEpoch++;
if (!view.value || !completionComp || !buildSqlCompletionExtension) return;
@@ -5879,6 +5916,7 @@ function pauseQueryEditorBackgroundWork() {
flushEditorViewport();
flushEditorSelection();
clearTableNavigationHover();
clearPendingCompletionEnter();
clearPendingCompletionTab();
executionViewportOwnership.reset();
editorIsActive = false;
+3
View File
@@ -6429,10 +6429,13 @@ export default {
vimModeDescription: "Use Vim-style modal editing in the SQL editor",
autoCloseBrackets: "Auto-close brackets",
autoCloseBracketsDescription: "Automatically insert closing brackets and quotes when typing an opening one",
sqlCompletionSection: "SQL completion",
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",
selectFirstCompletionOnOpen: "Automatically select the first completion",
selectFirstCompletionOnOpenDescription: "When enabled, the first candidate is selected when the list opens, and Enter or Tab accepts it. When disabled, Enter inserts a new line and Tab accepts the first candidate.",
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",
+3
View File
@@ -6115,6 +6115,9 @@ export default withEnglishFallback({
vimModeDescription: "Usar edición modal estilo Vim en el editor SQL",
autoCloseBrackets: "Cerrar paréntesis automáticamente",
autoCloseBracketsDescription: "Insertar automáticamente paréntesis y comillas de cierre al escribir los de apertura",
sqlCompletionSection: "Autocompletado SQL",
selectFirstCompletionOnOpen: "Seleccionar automáticamente la primera sugerencia",
selectFirstCompletionOnOpenDescription: "Al activarlo, se selecciona la primera sugerencia al abrir la lista y se acepta con Enter o Tab. Al desactivarlo, Enter inserta una nueva línea y Tab acepta la primera sugerencia.",
completionTriggerMode: "Modo de activación de autocompletado",
completionTriggerModeDescription: "Controla cuándo se abre automáticamente la ventana de autocompletado SQL. Manual requiere una tecla de acceso rápido; Prefijo escrito requiere un identificador escrito; Inteligente conserva el comportamiento actual.",
completionTriggerModeManual: "Solo manual",
+3
View File
@@ -6115,6 +6115,9 @@ export default withEnglishFallback({
vimModeDescription: "Usa la modifica modale in stile Vim nell'editor SQL",
autoCloseBrackets: "Chiusura automatica parentesi",
autoCloseBracketsDescription: "Inserisci automaticamente parentesi e virgolette di chiusura quando digiti quelle di apertura",
sqlCompletionSection: "Completamento SQL",
selectFirstCompletionOnOpen: "Seleziona automaticamente il primo suggerimento",
selectFirstCompletionOnOpenDescription: "Se attivo, il primo suggerimento viene selezionato all'apertura dell'elenco e accettato con Invio o Tab. Se disattivo, Invio inserisce una nuova riga e Tab accetta il primo suggerimento.",
completionTriggerMode: "Modalità di attivazione autocompletamento",
completionTriggerModeDescription: "Controlla quando si apre automaticamente il popup di autocompletamento SQL. Manuale richiede un tasto di scelta rapida; Prefisso digitato richiede un identificatore digitato; Intelligente mantiene il comportamento attuale.",
completionTriggerModeManual: "Solo manuale",
+3
View File
@@ -6518,8 +6518,11 @@ export default withEnglishFallback({
syncSnippetRetryLegacyCleanup: "旧スニペットのクリーンアップを再試行",
syncSnippetLegacyCleanupSuccess: "旧プレーンテキストスニペット {id} を削除しました。",
syncSnippetGuide: "設定ガイド",
sqlCompletionSection: "SQL 補完",
insertSpaceAfterCompletion: "補全後に自動でスペースを挿入",
insertSpaceAfterCompletionDescription: "キーワード、テーブル名、列名の補全確定時、次の文字が許容する場合に自動でスペースを補填します",
selectFirstCompletionOnOpen: "最初の補完候補を自動選択",
selectFirstCompletionOnOpenDescription: "有効にすると、候補一覧を開いた時に先頭項目を選択し、Enter または Tab で確定します。無効にすると、Enter は改行し、Tab で先頭候補を確定します。",
completionTriggerMode: "自動補完トリガーモード",
completionTriggerModeDescription: "SQL補完ポップアップが自動的に開くタイミングを制御します。手動はショートカットキーが必要; 識別子入力後は1文字以上の入力が必要; スマート位置は現在の動作を維持します。",
completionTriggerModeManual: "手動のみ",
+3
View File
@@ -5816,8 +5816,11 @@ export default withEnglishFallback({
vimModeDescription: "SQL 편집기에서 Vim 스타일의 모달 편집을 사용합니다",
autoCloseBrackets: "괄호 자동 닫기",
autoCloseBracketsDescription: "여는 괄호나 따옴표를 입력할 때 닫는 괄호와 따옴표를 자동으로 삽입합니다",
sqlCompletionSection: "SQL 자동 완성",
insertSpaceAfterCompletion: "완성 후 공백 삽입",
insertSpaceAfterCompletionDescription: "다음 문자가 허용할 때 키워드, 테이블 또는 컬럼 완성을 수락한 후 공백을 추가합니다",
selectFirstCompletionOnOpen: "첫 번째 자동 완성 후보 자동 선택",
selectFirstCompletionOnOpenDescription: "활성화하면 후보 목록이 열릴 때 첫 항목을 선택하고 Enter 또는 Tab으로 적용합니다. 비활성화하면 Enter는 줄을 바꾸고 Tab은 첫 후보를 적용합니다.",
completionTriggerMode: "자동 완성 트리거 모드",
completionTriggerModeDescription: "SQL 완성 팝업이 자동으로 열리는 시기를 제어합니다. 수동은 단축키 필요; 입력 식별자는 하나 이상의 문자 입력 필요; 스마트 위치는 현재 동작 유지.",
completionTriggerModeManual: "수동만",
+3
View File
@@ -6117,6 +6117,9 @@ export default withEnglishFallback({
vimModeDescription: "Usar edição modal no estilo Vim no editor SQL",
autoCloseBrackets: "Fechar parênteses automaticamente",
autoCloseBracketsDescription: "Inserir automaticamente parênteses e aspas de fechamento ao digitar os de abertura",
sqlCompletionSection: "Preenchimento de SQL",
selectFirstCompletionOnOpen: "Selecionar automaticamente a primeira sugestão",
selectFirstCompletionOnOpenDescription: "Quando ativado, a primeira sugestão é selecionada ao abrir a lista e aceita com Enter ou Tab. Quando desativado, Enter insere uma nova linha e Tab aceita a primeira sugestão.",
completionTriggerMode: "Modo de acionamento do autocompletar",
completionTriggerModeDescription: "Controla quando o popup de autocompletar SQL abre automaticamente. Manual requer uma tecla de atalho; Prefixo digitado requer um identificador digitado; Inteligente mantém o comportamento atual.",
completionTriggerModeManual: "Apenas manual",
+3
View File
@@ -6412,10 +6412,13 @@ export default withEnglishFallback({
vimModeDescription: "在 SQL 编辑器中使用 Vim 风格的模态编辑",
autoCloseBrackets: "自动成对补全",
autoCloseBracketsDescription: "输入左括号或左引号时自动补全对应的右括号或右引号",
sqlCompletionSection: "SQL 补全",
insertSpaceAfterCompletion: "补全后自动添加空格",
insertSpaceAfterCompletionDescription: "选择关键字、表名或列名补全后,在后续字符允许时自动追加空格",
sortCompletionColumnsAlphabetically: "按字母顺序排列补全字段",
sortCompletionColumnsAlphabeticallyDescription: "关闭后,表字段将保持数据库中的原始顺序",
selectFirstCompletionOnOpen: "自动选中首个补全候选",
selectFirstCompletionOnOpenDescription: "开启后,候选列表弹出时会选中第一项,可用 Enter 或 Tab 接受;关闭后,Enter 换行,Tab 接受第一项。",
completionTriggerMode: "自动补全触发方式",
completionTriggerModeDescription: "控制 SQL 补全弹窗何时自动打开。仅手动提示需快捷键;输入标识符后提示需输入至少一个字符;智能位置提示保持当前行为。",
completionTriggerModeManual: "仅手动提示",
+3
View File
@@ -5433,6 +5433,9 @@ export default withEnglishFallback({
vimModeDescription: "在 SQL 編輯器中使用 Vim 風格的模態編輯",
autoCloseBrackets: "自動成對補全",
autoCloseBracketsDescription: "輸入左括號或左引號時自動補全對應的右括號或右引號",
sqlCompletionSection: "SQL 補全",
selectFirstCompletionOnOpen: "自動選取第一個補全候選",
selectFirstCompletionOnOpenDescription: "開啟後,候選清單彈出時會選取第一項,可用 Enter 或 Tab 接受;關閉後,Enter 換行,Tab 接受第一項。",
completionTriggerMode: "自動補全觸發方式",
completionTriggerModeDescription: "控制 SQL 補全彈窗何時自動開啟。僅手動提示需快捷鍵;輸入識別碼後提示需輸入至少一個字元;智慧位置提示保持目前行為。",
completionTriggerModeManual: "僅手動提示",
@@ -1,6 +1,7 @@
import { readFileSync } from "node:fs";
import ts from "typescript";
import { afterEach, describe, expect, it, vi } from "vitest";
import { acceptSelectedCompletionWithRetry, acceptSelectedOrFirstCompletion } from "@/lib/editor/queryEditorCompletionAcceptance";
import { DEFAULT_SHORTCUT_SETTINGS, normalizeShortcutSettings, shortcutToCodeMirrorKey } from "@/lib/editor/shortcutRegistry";
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
@@ -47,6 +48,7 @@ interface MockView {
interface TabHarness {
handleTab: (view: MockView) => boolean;
handleEnter: (view: MockView) => boolean;
insertNewlineWithoutCompletion: (view: MockView) => boolean;
acceptCompletionOrNextSnippetField: (view: MockView) => boolean;
clearPendingCompletionTab: () => void;
@@ -56,24 +58,31 @@ interface TabHarness {
function createHarness(options: {
completionStatus: (state: MockState) => "active" | "pending" | null;
acceptCompletion?: (view: MockView) => boolean;
selectedCompletionIndex?: (state: MockState) => number | null;
selectFirstCompletion?: (view: MockView) => boolean;
closeCompletion?: (view: MockView) => boolean;
insertNewlineKeepIndent?: (view: MockView) => boolean;
nextSnippetField?: (view: MockView) => boolean;
indentMore?: (view: MockView) => boolean;
acceptCompletionShortcut?: string;
selectFirstCompletionOnOpen?: boolean;
}): TabHarness {
const source = [
extractDeclaration(/const COMPLETION_REMOTE_LATENCY_BUDGET_MS = \d+;/, "remote completion latency budget"),
extractDeclaration(/const COMPLETION_DEBOUNCE_DELAY_MS = \d+;/, "completion debounce delay"),
extractDeclaration(/const COMPLETION_TAB_RETRY_DELAY_MS = \d+;/, "completion retry delay"),
extractDeclaration(/const COMPLETION_TAB_MAX_WAIT_MS = [^;]+;/, "completion wait timeout"),
extractDeclaration(/const COMPLETION_ENTER_MAX_WAIT_MS = \d+;/, "completion Enter wait timeout"),
"let pendingCompletionTabTimer: ReturnType<typeof setTimeout> | null = null;",
"let cancelPendingCompletionEnter: (() => void) | null = null;",
"let suppressNextSqlCompletionAutoStartUntil = 0;",
extractFunction("editorIndentUnit"),
extractFunction("handleTab"),
extractFunction("tabKeyAcceptsCompletion"),
extractFunction("handleTabWithoutAcceptingCompletion"),
extractFunction("performNormalTab"),
extractFunction("handleEnter"),
extractFunction("clearPendingCompletionEnter"),
extractFunction("insertNewlineWithoutCompletion"),
extractFunction("acceptCompletionOrNextSnippetField"),
extractFunction("clearPendingCompletionTab"),
@@ -86,6 +95,10 @@ function createHarness(options: {
const factory = new Function(
"codeMirrorCompletionStatus",
"codeMirrorAcceptCompletion",
"codeMirrorSelectedCompletionIndex",
"codeMirrorSelectFirstCompletion",
"acceptSelectedCompletionWithRetry",
"acceptSelectedOrFirstCompletion",
"codeMirrorCloseCompletion",
"codeMirrorInsertNewlineKeepIndent",
"insertQueryEditorNewline",
@@ -95,11 +108,15 @@ function createHarness(options: {
"normalizeShortcutSettings",
"shortcutToCodeMirrorKey",
"props",
`${javascript}\nreturn { handleTab, insertNewlineWithoutCompletion, acceptCompletionOrNextSnippetField, clearPendingCompletionTab, consumeSqlCompletionAutoStartSuppression };`,
`${javascript}\nreturn { handleTab, handleEnter, insertNewlineWithoutCompletion, acceptCompletionOrNextSnippetField, clearPendingCompletionTab, consumeSqlCompletionAutoStartSuppression };`,
);
return factory(
options.completionStatus,
options.acceptCompletion ?? (() => false),
options.selectedCompletionIndex ?? (() => 0),
options.selectFirstCompletion ?? (() => false),
acceptSelectedCompletionWithRetry,
acceptSelectedOrFirstCompletion,
options.closeCompletion ?? (() => false),
options.insertNewlineKeepIndent ?? (() => false),
(view: MockView, fallback: ((view: MockView) => boolean) | null | undefined) => fallback?.(view) ?? false,
@@ -109,6 +126,7 @@ function createHarness(options: {
editorSettings: {
sqlFormatter: { useTabs: false, tabWidth: 2 },
shortcuts: { ...DEFAULT_SHORTCUT_SETTINGS, acceptCompletion: options.acceptCompletionShortcut ?? DEFAULT_SHORTCUT_SETTINGS.acceptCompletion },
selectFirstCompletionOnOpen: options.selectFirstCompletionOnOpen ?? false,
},
},
normalizeShortcutSettings,
@@ -156,6 +174,19 @@ describe("QueryEditor completion Tab keymap", () => {
expect(harness.consumeSqlCompletionAutoStartSuppression()).toBe(false);
});
it("accepts the selected completion on Enter only in first-option mode", () => {
const acceptCompletion = vi.fn(() => true);
const closeCompletion = vi.fn(() => true);
const insertNewlineKeepIndent = vi.fn(() => true);
const harness = createHarness({ completionStatus: () => "active", acceptCompletion, closeCompletion, insertNewlineKeepIndent, selectFirstCompletionOnOpen: true });
const view = createView();
expect(harness.handleEnter(view)).toBe(true);
expect(acceptCompletion).toHaveBeenCalledWith(view);
expect(closeCompletion).not.toHaveBeenCalled();
expect(insertNewlineKeepIndent).not.toHaveBeenCalled();
});
it("does not suppress later completion when Enter cannot insert a newline", () => {
const harness = createHarness({ completionStatus: () => null, insertNewlineKeepIndent: () => false });
@@ -2,14 +2,16 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { acceptCompletion, autocompletion, completionStatus, currentCompletions, selectedCompletionIndex, startCompletion } from "@codemirror/autocomplete";
import { EditorState, Transaction } from "@codemirror/state";
import { acceptCompletion, autocompletion, completionStatus, currentCompletions, moveCompletionSelection, selectedCompletionIndex, startCompletion } from "@codemirror/autocomplete";
import { EditorState, Prec, Transaction } from "@codemirror/state";
import { insertNewlineKeepIndent } from "@codemirror/commands";
import { EditorView, keymap, runScopeHandlers } from "@codemirror/view";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { acceptSelectedCompletionWithRetry, acceptSelectedOrFirstCompletion } from "@/lib/editor/queryEditorCompletionAcceptance";
const queryEditorSource = readFileSync(resolve(process.cwd(), "apps/desktop/src/components/editor/QueryEditor.vue"), "utf8");
function createCompletionView(doc = "") {
function createCompletionView(doc = "", selectFirstCompletionOnOpen = false, interactionDelay: number | null = 0) {
return new EditorView({
parent: document.createElement("div"),
state: EditorState.create({
@@ -19,11 +21,36 @@ function createCompletionView(doc = "") {
autocompletion({
activateOnTyping: true,
activateOnTypingDelay: 0,
interactionDelay: 0,
selectOnOpen: false,
...(interactionDelay === null ? {} : { interactionDelay }),
selectOnOpen: selectFirstCompletionOnOpen,
override: [() => ({ from: 0, options: [{ label: "select" }, { label: "set" }] })],
}),
keymap.of([{ key: "Tab", run: acceptCompletion }]),
Prec.highest(
keymap.of([
{
key: "Tab",
run: (view) => acceptSelectedOrFirstCompletion(view, acceptCompletion, selectedCompletionIndex, moveCompletionSelection(true)),
},
{
key: "Enter",
run: (view) => {
if (selectFirstCompletionOnOpen) {
const result = acceptSelectedCompletionWithRetry(view, {
completionStatus,
acceptCompletion,
selectedCompletionIndex,
selectFirstCompletion: moveCompletionSelection(true),
retryDelayMs: 16,
maxWaitMs: 125,
onUnavailable: () => insertNewlineKeepIndent(view),
});
if (result.handled) return true;
}
return insertNewlineKeepIndent(view);
},
},
]),
),
],
}),
});
@@ -41,34 +68,84 @@ async function expectCompletionOpenWithoutSelection(view: EditorView) {
describe("QueryEditor completion selection", () => {
it("binds SQL completion to CodeMirror's unselected-on-open mode", () => {
expect(queryEditorSource).toMatch(/buildSqlCompletionExtension = \(\) =>\s+autocompletion\(\{\s+activateOnTyping: true,\s+selectOnOpen: false,/);
expect(queryEditorSource).toMatch(/selectOnOpen: settingsStore\.editorSettings\.selectFirstCompletionOnOpen/);
});
it("opens manual completion unselected until ArrowDown selects the first option", async () => {
it("opens manual completion unselected and accepts the first option with Tab", async () => {
const view = createCompletionView();
expect(startCompletion(view)).toBe(true);
await expectCompletionOpenWithoutSelection(view);
expect(press(view, "Tab")).toBe(false);
expect(view.state.doc.toString()).toBe("");
expect(press(view, "ArrowDown")).toBe(true);
expect(selectedCompletionIndex(view.state)).toBe(0);
expect(press(view, "Tab")).toBe(true);
expect(view.state.doc.toString()).toBe("select");
view.destroy();
});
it("accepts an explicitly selected option instead of resetting to the first option", async () => {
const view = createCompletionView();
expect(startCompletion(view)).toBe(true);
await expectCompletionOpenWithoutSelection(view);
expect(press(view, "ArrowUp")).toBe(true);
expect(selectedCompletionIndex(view.state)).toBe(1);
expect(press(view, "Tab")).toBe(true);
expect(view.state.doc.toString()).toBe("set");
view.destroy();
});
it("opens typing-triggered completion without selecting the first option", async () => {
const view = createCompletionView();
view.dispatch({
changes: { from: 0, insert: "s" },
selection: { anchor: 1 },
annotations: Transaction.userEvent.of("input.type"),
});
await expectCompletionOpenWithoutSelection(view);
expect(press(view, "Tab")).toBe(false);
expect(view.state.doc.toString()).toBe("s");
expect(press(view, "Tab")).toBe(true);
expect(view.state.doc.toString()).toBe("select");
view.destroy();
});
it("keeps Enter as a newline while no completion option is selected", async () => {
const view = createCompletionView();
expect(startCompletion(view)).toBe(true);
await expectCompletionOpenWithoutSelection(view);
expect(press(view, "Enter")).toBe(true);
expect(view.state.doc.toString()).toBe("\n");
view.destroy();
});
it("restores first-option selection and Enter acceptance when explicitly enabled", async () => {
const view = createCompletionView("", true);
expect(startCompletion(view)).toBe(true);
await expect.poll(() => completionStatus(view.state)).toBe("active");
expect(selectedCompletionIndex(view.state)).toBe(0);
expect(press(view, "Enter")).toBe(true);
expect(view.state.doc.toString()).toBe("select");
view.destroy();
});
it("waits through CodeMirror's default interaction delay before accepting Enter", async () => {
let now = 0;
const dateNow = vi.spyOn(Date, "now").mockImplementation(() => now);
const view = createCompletionView("", true, null);
try {
expect(startCompletion(view)).toBe(true);
await expect.poll(() => completionStatus(view.state), { interval: 1 }).toBe("active");
expect(selectedCompletionIndex(view.state)).toBe(0);
expect(acceptCompletion(view)).toBe(false);
expect(press(view, "Enter")).toBe(true);
expect(view.state.doc.toString()).toBe("");
now = 100;
await expect.poll(() => view.state.doc.toString(), { interval: 10 }).toBe("select");
} finally {
dateNow.mockRestore();
view.destroy();
}
});
});
@@ -0,0 +1,77 @@
import type { EditorState } from "@codemirror/state";
import type { Command, EditorView } from "@codemirror/view";
type SelectedCompletionIndex = (state: EditorState) => number | null;
type CompletionStatus = (state: EditorState) => "active" | "pending" | null;
interface RetryCompletionAcceptanceOptions {
completionStatus: CompletionStatus;
acceptCompletion: Command | null;
selectedCompletionIndex: SelectedCompletionIndex | null;
selectFirstCompletion: Command | null;
retryDelayMs: number;
maxWaitMs: number;
onUnavailable: () => void;
onSettled?: () => void;
}
interface CompletionAcceptanceAttempt {
handled: boolean;
cancel?: () => void;
}
export function acceptSelectedOrFirstCompletion(view: EditorView, acceptCompletion: Command | null, selectedCompletionIndex: SelectedCompletionIndex | null, selectFirstCompletion: Command | null): boolean {
if (!acceptCompletion) return false;
if (!selectedCompletionIndex || !selectFirstCompletion) return acceptCompletion(view);
if (selectedCompletionIndex(view.state) !== null) return acceptCompletion(view);
if (!selectFirstCompletion(view)) return false;
return acceptCompletion(view);
}
export function acceptSelectedCompletionWithRetry(view: EditorView, options: RetryCompletionAcceptanceOptions): CompletionAcceptanceAttempt {
if (options.completionStatus(view.state) !== "active") return { handled: false };
if (acceptSelectedOrFirstCompletion(view, options.acceptCompletion, options.selectedCompletionIndex, options.selectFirstCompletion)) return { handled: true };
const initialDoc = view.state.doc;
const initialSelectionRanges = view.state.selection.ranges.map((range) => ({ anchor: range.anchor, head: range.head }));
const startedAt = Date.now();
let timer: ReturnType<typeof setTimeout> | null = null;
let settled = false;
const settle = () => {
if (settled) return;
settled = true;
if (timer !== null) {
clearTimeout(timer);
timer = null;
}
options.onSettled?.();
};
const retry = () => {
timer = null;
if (settled) return;
const selectionRanges = view.state.selection.ranges;
if (view.state.doc !== initialDoc || selectionRanges.length !== initialSelectionRanges.length || selectionRanges.some((range, index) => range.anchor !== initialSelectionRanges[index]?.anchor || range.head !== initialSelectionRanges[index]?.head)) {
settle();
return;
}
const completionStatus = options.completionStatus(view.state);
if (completionStatus === "active" && acceptSelectedOrFirstCompletion(view, options.acceptCompletion, options.selectedCompletionIndex, options.selectFirstCompletion)) {
settle();
return;
}
if (completionStatus && Date.now() - startedAt < options.maxWaitMs) {
timer = setTimeout(retry, options.retryDelayMs);
return;
}
settle();
options.onUnavailable();
};
timer = setTimeout(retry, options.retryDelayMs);
return { handled: true, cancel: settle };
}
@@ -22,6 +22,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"autoAliasTables",
"insertSpaceAfterCompletion",
"sortCompletionColumnsAlphabetically",
"selectFirstCompletionOnOpen",
"wordWrap",
"vimModeEnabled",
"autoCloseBrackets",
@@ -132,6 +132,13 @@ describe("normalizeEditorSettings", () => {
expect(normalizeEditorSettings({ insertSpaceAfterCompletion: false }).insertSpaceAfterCompletion).toBe(false);
});
it("keeps non-intrusive completion selection as the default", () => {
expect(normalizeEditorSettings({}).selectFirstCompletionOnOpen).toBe(false);
expect(normalizeEditorSettings({ selectFirstCompletionOnOpen: true }).selectFirstCompletionOnOpen).toBe(true);
expect(normalizeEditorSettings({ selectFirstCompletionOnOpen: false }).selectFirstCompletionOnOpen).toBe(false);
expect(normalizeEditorSettings({ selectFirstCompletionOnOpen: "true" } as any).selectFirstCompletionOnOpen).toBe(false);
});
it("defaults sidebar connection sorting to manual order and preserves valid alphabetical modes", () => {
expect(normalizeEditorSettings({}).sidebarConnectionSortMode).toBe("manual");
expect(normalizeEditorSettings({ sidebarConnectionSortMode: "asc" }).sidebarConnectionSortMode).toBe("asc");
+4
View File
@@ -531,6 +531,7 @@ export interface EditorSettings {
autoAliasTables: boolean;
insertSpaceAfterCompletion: boolean;
sortCompletionColumnsAlphabetically: boolean;
selectFirstCompletionOnOpen: boolean;
wordWrap: boolean;
tableDdlWordWrap: boolean;
vimModeEnabled: boolean;
@@ -742,6 +743,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
autoAliasTables: true,
insertSpaceAfterCompletion: true,
sortCompletionColumnsAlphabetically: true,
selectFirstCompletionOnOpen: false,
wordWrap: false,
tableDdlWordWrap: true,
vimModeEnabled: false,
@@ -1117,6 +1119,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
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,
selectFirstCompletionOnOpen: typeof settings.selectFirstCompletionOnOpen === "boolean" ? settings.selectFirstCompletionOnOpen : DEFAULT_EDITOR_SETTINGS.selectFirstCompletionOnOpen,
wordWrap: settings.wordWrap ?? DEFAULT_EDITOR_SETTINGS.wordWrap,
tableDdlWordWrap: typeof settings.tableDdlWordWrap === "boolean" ? settings.tableDdlWordWrap : DEFAULT_EDITOR_SETTINGS.tableDdlWordWrap,
vimModeEnabled: typeof settings.vimModeEnabled === "boolean" ? settings.vimModeEnabled : DEFAULT_EDITOR_SETTINGS.vimModeEnabled,
@@ -1720,6 +1723,7 @@ export const useSettingsStore = defineStore("settings", () => {
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.selectFirstCompletionOnOpen !== undefined) editorSettings.value.selectFirstCompletionOnOpen = partial.selectFirstCompletionOnOpen === true;
if (partial.wordWrap !== undefined) editorSettings.value.wordWrap = partial.wordWrap;
if (partial.tableDdlWordWrap !== undefined) editorSettings.value.tableDdlWordWrap = partial.tableDdlWordWrap === true;
if (partial.vimModeEnabled !== undefined) editorSettings.value.vimModeEnabled = partial.vimModeEnabled === true;