feat(completion): configurable SQL completion trigger modes
This commit is contained in:
parent
bad7f6b472
commit
5b85f9eae8
|
|
@ -46,6 +46,7 @@ import {
|
|||
type CustomThemeColors,
|
||||
type CustomTheme,
|
||||
type ClickTableNavigationTarget,
|
||||
type SqlCompletionTriggerMode,
|
||||
} from "@/stores/settingsStore";
|
||||
import { createRunStatementButtonDom, loadEditorTheme, editorFontTheme } from "@/lib/editor/editorThemes";
|
||||
import { orderAiConfigsForDisplay } from "@/lib/ai/aiConfigOrdering";
|
||||
|
|
@ -277,6 +278,7 @@ const editShowCurrentStatementFrame = ref(settingsStore.editorSettings.showCurre
|
|||
const editShowInsertValueHints = ref(settingsStore.editorSettings.showInsertValueHints);
|
||||
const editAutoAliasTables = ref(settingsStore.editorSettings.autoAliasTables);
|
||||
const editInsertSpaceAfterCompletion = ref(settingsStore.editorSettings.insertSpaceAfterCompletion);
|
||||
const editCompletionTriggerMode = ref<SqlCompletionTriggerMode>(settingsStore.editorSettings.completionTriggerMode);
|
||||
const editWordWrap = ref(settingsStore.editorSettings.wordWrap);
|
||||
const editVimModeEnabled = ref(settingsStore.editorSettings.vimModeEnabled);
|
||||
const editAutoCloseBrackets = ref(settingsStore.editorSettings.autoCloseBrackets);
|
||||
|
|
@ -445,6 +447,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
|
|||
showInsertValueHints: editShowInsertValueHints.value,
|
||||
autoAliasTables: editAutoAliasTables.value,
|
||||
insertSpaceAfterCompletion: editInsertSpaceAfterCompletion.value,
|
||||
completionTriggerMode: editCompletionTriggerMode.value,
|
||||
wordWrap: editWordWrap.value,
|
||||
vimModeEnabled: editVimModeEnabled.value,
|
||||
autoCloseBrackets: editAutoCloseBrackets.value,
|
||||
|
|
@ -712,6 +715,7 @@ function syncEditorSettingsDraftFromStore() {
|
|||
editShowInsertValueHints.value = settingsStore.editorSettings.showInsertValueHints;
|
||||
editAutoAliasTables.value = settingsStore.editorSettings.autoAliasTables;
|
||||
editInsertSpaceAfterCompletion.value = settingsStore.editorSettings.insertSpaceAfterCompletion;
|
||||
editCompletionTriggerMode.value = settingsStore.editorSettings.completionTriggerMode;
|
||||
editWordWrap.value = settingsStore.editorSettings.wordWrap;
|
||||
editVimModeEnabled.value = settingsStore.editorSettings.vimModeEnabled;
|
||||
editAutoCloseBrackets.value = settingsStore.editorSettings.autoCloseBrackets;
|
||||
|
|
@ -915,6 +919,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
|
|||
editShowInsertValueHints.value = DEFAULT_EDITOR_SETTINGS.showInsertValueHints;
|
||||
editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables;
|
||||
editInsertSpaceAfterCompletion.value = DEFAULT_EDITOR_SETTINGS.insertSpaceAfterCompletion;
|
||||
editCompletionTriggerMode.value = DEFAULT_EDITOR_SETTINGS.completionTriggerMode;
|
||||
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
|
||||
editVimModeEnabled.value = DEFAULT_EDITOR_SETTINGS.vimModeEnabled;
|
||||
editAutoCloseBrackets.value = DEFAULT_EDITOR_SETTINGS.autoCloseBrackets;
|
||||
|
|
@ -1193,6 +1198,12 @@ function onExecuteModeChange(v: any) {
|
|||
if (v === "all" || v === "current") editExecuteMode.value = v;
|
||||
}
|
||||
|
||||
function onCompletionTriggerModeChange(v: any) {
|
||||
if (v === "manual" || v === "require-prefix" || v === "positional") {
|
||||
editCompletionTriggerMode.value = v;
|
||||
}
|
||||
}
|
||||
|
||||
function onSqlSemanticDiagnosticsEnabledChange(value: boolean) {
|
||||
editSqlSemanticDiagnosticsEnabled.value = value;
|
||||
editSqlSemanticDiagnosticsMode.value = value ? "enabled" : "disabled";
|
||||
|
|
@ -3722,6 +3733,23 @@ onUnmounted(() => {
|
|||
</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>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import {
|
|||
shouldChainSqlCompletionAfterAccept,
|
||||
extractCteDefinitions,
|
||||
} from "@/lib/sql/sqlCompletion";
|
||||
import { originForSqlCompletionProvider, originForTypedSqlCompletionStart, shouldAllowSqlCompletionTrigger, type SqlCompletionTriggerFacts, type SqlCompletionTriggerOrigin } from "@/lib/sql/sqlCompletionTriggerPolicy";
|
||||
import { sqlCompletionContextFromSemantic, sqlSemanticSelectStarIsOnlyProjection, sqlSemanticSelectStarQualifierSql, sqlSemanticSelectStarTableSource } from "@/lib/sql/semantic/completion";
|
||||
import { buildSqlSemanticModel } from "@/lib/sql/semantic/model";
|
||||
import { mergeSqlSemanticReferenceAnalysis, resolveSqlSemanticNavigationTarget } from "@/lib/sql/semantic/references";
|
||||
|
|
@ -403,6 +404,7 @@ let codeMirrorSnippetCompletion: typeof import("@codemirror/autocomplete").snipp
|
|||
let codeMirrorCompletionStatus: typeof import("@codemirror/autocomplete").completionStatus | null = null;
|
||||
let codeMirrorAcceptCompletion: typeof import("@codemirror/autocomplete").acceptCompletion | 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;
|
||||
let codeMirrorNextSnippetField: typeof import("@codemirror/autocomplete").nextSnippetField | null = null;
|
||||
let codeMirrorIndentMore: typeof import("@codemirror/commands").indentMore | null = null;
|
||||
|
|
@ -2665,6 +2667,7 @@ let completionEpoch = 0;
|
|||
let completionDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let typedCompletionActivationUntil = 0;
|
||||
let suppressNextSqlCompletionAutoStartUntil = 0;
|
||||
let activeCompletionOrigin: SqlCompletionTriggerOrigin | null = null;
|
||||
|
||||
type QueryCompletionItem = SqlCompletionItem | ElasticsearchCompletionItem | RedisCompletionItem | MongoCompletionItem;
|
||||
|
||||
|
|
@ -2939,13 +2942,56 @@ async function provideSqlCompletions(context: CompletionContext) {
|
|||
const epoch = ++completionEpoch;
|
||||
|
||||
try {
|
||||
// 1. Suppressed context (comment / string literal) rejects everything, including explicit.
|
||||
if (isSqlCompletionSuppressedContext(fullDoc, position)) return null;
|
||||
|
||||
// 2. Determine completion origin (session-level marker).
|
||||
activeCompletionOrigin = originForSqlCompletionProvider(activeCompletionOrigin, context.explicit);
|
||||
const origin = activeCompletionOrigin;
|
||||
|
||||
// 3. Explicit (manual shortcut) -> always proceed. No mode gating.
|
||||
// 4. For typing sessions, apply mode gating with lazy fact computation.
|
||||
const useDatabaseCompletion = resolveSqlServerUseDatabaseCompletion({
|
||||
sql: fullDoc,
|
||||
cursor: position,
|
||||
databaseType: props.databaseType,
|
||||
});
|
||||
if (!explicit && !useDatabaseCompletion && !shouldAutoOpenSqlCompletion(fullDoc, position, sqlCompletionDialectOptions())) return null;
|
||||
const useDatabasePrefix = useDatabaseCompletion?.prefix ?? null;
|
||||
|
||||
if (origin !== "explicit") {
|
||||
const mode = settingsStore.editorSettings.completionTriggerMode;
|
||||
|
||||
// manual: never auto-open. Return before computing any context.
|
||||
if (mode === "manual") return null;
|
||||
|
||||
// require-prefix: only compute local facts (no positionalEligible).
|
||||
if (mode === "require-prefix") {
|
||||
const ctx = getSqlCompletionContext(fullDoc, position, sqlCompletionDialectOptions());
|
||||
const prevChar = fullDoc[position - 1] ?? "";
|
||||
const facts: SqlCompletionTriggerFacts = {
|
||||
origin,
|
||||
hasIdentifierPrefix: ctx.prefix.length > 0,
|
||||
qualifierTriggered: prevChar === "." && ctx.qualifier != null,
|
||||
useDatabasePrefix,
|
||||
};
|
||||
if (!shouldAllowSqlCompletionTrigger(mode, facts)) return null;
|
||||
}
|
||||
|
||||
// positional: compute positionalEligible (lazy).
|
||||
if (mode === "positional") {
|
||||
const ctx = getSqlCompletionContext(fullDoc, position, sqlCompletionDialectOptions());
|
||||
const prevChar = fullDoc[position - 1] ?? "";
|
||||
const positionalEligible = shouldAutoOpenSqlCompletion(fullDoc, position, sqlCompletionDialectOptions());
|
||||
const facts: SqlCompletionTriggerFacts = {
|
||||
origin,
|
||||
hasIdentifierPrefix: ctx.prefix.length > 0,
|
||||
qualifierTriggered: prevChar === "." && ctx.qualifier != null,
|
||||
useDatabasePrefix,
|
||||
positionalEligible,
|
||||
};
|
||||
if (!shouldAllowSqlCompletionTrigger(mode, facts)) return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (useDatabaseCompletion) {
|
||||
const currentDatabase = props.database ?? "";
|
||||
|
|
@ -3111,6 +3157,7 @@ function scheduleSqlCompletionStart(currentView: EditorViewType, delayMs = 0) {
|
|||
window.setTimeout(() => {
|
||||
if (!codeMirrorStartCompletion || isEditorComposing(currentView)) return;
|
||||
markTypedCompletionActivation();
|
||||
activeCompletionOrigin = originForTypedSqlCompletionStart(activeCompletionOrigin);
|
||||
codeMirrorStartCompletion(currentView);
|
||||
}, delayMs);
|
||||
}
|
||||
|
|
@ -3129,34 +3176,83 @@ function flushImeComposition() {
|
|||
if (editorIsActive) emitEditorSelection(latestSelection);
|
||||
const fullDoc = currentView.state.doc.toString();
|
||||
const position = currentView.state.selection.main.head;
|
||||
if (resolveSqlServerUseDatabaseCompletion({ sql: fullDoc, cursor: position, databaseType: props.databaseType }) || shouldAutoOpenSqlCompletion(fullDoc, position, sqlCompletionDialectOptions())) {
|
||||
if (shouldTriggerSqlCompletionForPosition(fullDoc, position)) {
|
||||
scheduleSqlCompletionStart(currentView);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the current SQL position should trigger completion under the active trigger mode.
|
||||
* Used by flushImeComposition and shouldStartSqlCompletionAfterInput.
|
||||
*/
|
||||
function shouldTriggerSqlCompletionForPosition(fullDoc: string, position: number): boolean {
|
||||
if (isSqlCompletionSuppressedContext(fullDoc, position)) return false;
|
||||
const mode = settingsStore.editorSettings.completionTriggerMode;
|
||||
if (mode === "manual") return false;
|
||||
|
||||
const useDatabaseCompletion = resolveSqlServerUseDatabaseCompletion({
|
||||
sql: fullDoc,
|
||||
cursor: position,
|
||||
databaseType: props.databaseType,
|
||||
});
|
||||
const useDatabasePrefix = useDatabaseCompletion?.prefix ?? null;
|
||||
|
||||
if (mode === "require-prefix") {
|
||||
const ctx = getSqlCompletionContext(fullDoc, position, sqlCompletionDialectOptions());
|
||||
const prevChar = fullDoc[position - 1] ?? "";
|
||||
const facts: SqlCompletionTriggerFacts = {
|
||||
origin: "typing",
|
||||
hasIdentifierPrefix: ctx.prefix.length > 0,
|
||||
qualifierTriggered: prevChar === "." && ctx.qualifier != null,
|
||||
useDatabasePrefix,
|
||||
};
|
||||
return shouldAllowSqlCompletionTrigger(mode, facts);
|
||||
}
|
||||
|
||||
// positional
|
||||
const ctx = getSqlCompletionContext(fullDoc, position, sqlCompletionDialectOptions());
|
||||
const prevChar = fullDoc[position - 1] ?? "";
|
||||
const positionalEligible = shouldAutoOpenSqlCompletion(fullDoc, position, sqlCompletionDialectOptions());
|
||||
const facts: SqlCompletionTriggerFacts = {
|
||||
origin: "typing",
|
||||
hasIdentifierPrefix: ctx.prefix.length > 0,
|
||||
qualifierTriggered: prevChar === "." && ctx.qualifier != null,
|
||||
useDatabasePrefix,
|
||||
positionalEligible,
|
||||
};
|
||||
return shouldAllowSqlCompletionTrigger(mode, facts);
|
||||
}
|
||||
|
||||
function shouldStartSqlCompletionAfterInput(insertedText: string, removedText: string, currentView: EditorViewType): boolean {
|
||||
const position = currentView.state.selection.main.head;
|
||||
const fullDoc = currentView.state.doc.toString();
|
||||
if (resolveSqlServerUseDatabaseCompletion({ sql: fullDoc, cursor: position, databaseType: props.databaseType })) return true;
|
||||
|
||||
// Non-SQL providers: keep existing behavior (trigger mode policy does not apply).
|
||||
if (props.databaseType === "mongodb") {
|
||||
return !!(insertedText || removedText) && shouldAutoOpenMongoCompletion(fullDoc, position);
|
||||
}
|
||||
if (props.databaseType === "victoriametrics") return false;
|
||||
if (!insertedText && removedText) {
|
||||
if (props.databaseType === "redis" || props.databaseType === "elasticsearch" || props.databaseType === "easysearch") {
|
||||
// Preserve old character-based checks for non-SQL providers.
|
||||
if (!insertedText && removedText) {
|
||||
const completionContext = getSqlCompletionContext(fullDoc, position, sqlCompletionDialectOptions());
|
||||
return isTableNameCompletionContext(completionContext) && shouldAutoOpenSqlCompletion(fullDoc, position, sqlCompletionDialectOptions());
|
||||
}
|
||||
if (insertedText.endsWith(".")) return true;
|
||||
if (/[,(]$/.test(insertedText)) {
|
||||
const completionContext = getSqlCompletionContext(fullDoc, position, sqlCompletionDialectOptions());
|
||||
return !!completionContext.insertTable;
|
||||
}
|
||||
if (/\s$/.test(insertedText)) {
|
||||
return shouldAutoOpenSqlCompletion(fullDoc, position, sqlCompletionDialectOptions());
|
||||
}
|
||||
if (!/[\w$@]$/.test(insertedText)) return false;
|
||||
const completionContext = getSqlCompletionContext(fullDoc, position, sqlCompletionDialectOptions());
|
||||
return isTableNameCompletionContext(completionContext) && shouldAutoOpenSqlCompletion(fullDoc, position, sqlCompletionDialectOptions());
|
||||
return isTableNameCompletionContext(completionContext) || shouldAutoOpenSqlCompletion(fullDoc, position, sqlCompletionDialectOptions());
|
||||
}
|
||||
if (insertedText.endsWith(".")) return true;
|
||||
if (/[,(]$/.test(insertedText)) {
|
||||
const completionContext = getSqlCompletionContext(fullDoc, position, sqlCompletionDialectOptions());
|
||||
return !!completionContext.insertTable;
|
||||
}
|
||||
if (/\s$/.test(insertedText)) {
|
||||
return shouldAutoOpenSqlCompletion(fullDoc, position, sqlCompletionDialectOptions());
|
||||
}
|
||||
if (!/[\w$@]$/.test(insertedText)) return false;
|
||||
const completionContext = getSqlCompletionContext(fullDoc, position, sqlCompletionDialectOptions());
|
||||
return isTableNameCompletionContext(completionContext) || shouldAutoOpenSqlCompletion(fullDoc, position, sqlCompletionDialectOptions());
|
||||
|
||||
// SQL providers: use unified trigger mode policy.
|
||||
return shouldTriggerSqlCompletionForPosition(fullDoc, position);
|
||||
}
|
||||
|
||||
function buildLocalSqlCompletionResult(completionContext: ReturnType<typeof getSqlCompletionContext>, fullDoc: string, position: number, scope: CompletionMetadataScope) {
|
||||
|
|
@ -3831,7 +3927,7 @@ onMounted(async () => {
|
|||
{ EditorView, keymap, rectangularSelection, hoverTooltip, showTooltip, closeHoverTooltips, Decoration, tooltips, gutter, GutterMarker, lineNumberMarkers, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, crosshairCursor, scrollPastEnd, ViewPlugin },
|
||||
{ EditorState, EditorSelection, Compartment, Prec, RangeSet, StateEffect, StateField },
|
||||
langSql,
|
||||
{ autocompletion, startCompletion, acceptCompletion, closeBrackets, closeBracketsKeymap, snippetCompletion, completionStatus, completionKeymap, insertCompletionText, nextSnippetField },
|
||||
{ autocompletion, startCompletion, acceptCompletion, closeBrackets, closeBracketsKeymap, snippetCompletion, completionStatus, completionKeymap, insertCompletionText, nextSnippetField, closeCompletion },
|
||||
{ copyLineDown, copyLineUp, deleteLine, indentLess, indentMore, insertNewlineKeepIndent, moveLineDown, moveLineUp, redo, selectAll, undo, toggleLineComment, history, defaultKeymap, historyKeymap },
|
||||
{ bracketMatching, foldGutter, indentOnInput, indentUnit, syntaxHighlighting, defaultHighlightStyle, foldKeymap, toggleFold, ensureSyntaxTree },
|
||||
{ searchKeymap },
|
||||
|
|
@ -3865,6 +3961,7 @@ onMounted(async () => {
|
|||
setSqlDiagnosticsEffect = StateEffect.define<SqlSemanticDiagnostic[]>();
|
||||
codeMirrorCompletionStatus = completionStatus;
|
||||
codeMirrorAcceptCompletion = acceptCompletion;
|
||||
codeMirrorCloseCompletion = closeCompletion;
|
||||
codeMirrorStartCompletion = startCompletion;
|
||||
codeMirrorInsertCompletionText = insertCompletionText;
|
||||
codeMirrorNextSnippetField = nextSnippetField;
|
||||
|
|
@ -4398,6 +4495,13 @@ onMounted(async () => {
|
|||
latestSelection = readEditorSelection(update.view);
|
||||
if (editorIsActive) emitEditorSelection(latestSelection);
|
||||
}
|
||||
// Clear activeCompletionOrigin when the completion session ends.
|
||||
if (codeMirrorCompletionStatus) {
|
||||
const status = codeMirrorCompletionStatus(update.state) ?? null;
|
||||
if (status === null) {
|
||||
activeCompletionOrigin = null;
|
||||
}
|
||||
}
|
||||
}),
|
||||
fontThemeComp.of(
|
||||
editorFontTheme(EditorView, liveFontSize.value, initialSettings.fontFamily, {
|
||||
|
|
@ -4749,6 +4853,29 @@ onMounted(async () => {
|
|||
});
|
||||
});
|
||||
|
||||
// When completionTriggerMode changes, close any open typing session
|
||||
// that would no longer be allowed under the new mode.
|
||||
watch(
|
||||
() => settingsStore.editorSettings.completionTriggerMode,
|
||||
(newMode) => {
|
||||
if (!view.value || !codeMirrorCompletionStatus || !codeMirrorCloseCompletion) return;
|
||||
const status = codeMirrorCompletionStatus(view.value.state);
|
||||
if (!status || activeCompletionOrigin !== "typing") return;
|
||||
// If switching to manual, close all typing sessions.
|
||||
if (newMode === "manual") {
|
||||
codeMirrorCloseCompletion(view.value);
|
||||
return;
|
||||
}
|
||||
// For other mode changes, re-evaluate the policy.
|
||||
// If the current position would not trigger under the new mode, close.
|
||||
const fullDoc = view.value.state.doc.toString();
|
||||
const position = view.value.state.selection.main.head;
|
||||
if (!shouldTriggerSqlCompletionForPosition(fullDoc, position)) {
|
||||
codeMirrorCloseCompletion(view.value);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
|
|
|
|||
|
|
@ -5335,6 +5335,11 @@ export default {
|
|||
autoCloseBracketsDescription: "Automatically insert closing brackets and quotes when typing an opening one",
|
||||
insertSpaceAfterCompletion: "Insert a space after completion",
|
||||
insertSpaceAfterCompletionDescription: "Append a space after accepting a keyword, table, or column completion when the next character allows it",
|
||||
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",
|
||||
completionTriggerModeRequirePrefix: "Typed identifier prefix",
|
||||
completionTriggerModePositional: "Smart position",
|
||||
sqlSemanticDiagnosticsEnabled: "SQL semantic diagnostics",
|
||||
sqlSemanticDiagnosticsEnabledDescription: "When enabled, the editor reports semantic issues such as unknown tables and columns. Disable it to reduce parsing and metadata checks for large SQL.",
|
||||
confirmDangerousSqlExecution: "Confirm before dangerous operations",
|
||||
|
|
|
|||
|
|
@ -5064,6 +5064,11 @@ 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",
|
||||
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",
|
||||
completionTriggerModeRequirePrefix: "Prefijo de identificador escrito",
|
||||
completionTriggerModePositional: "Posición inteligente",
|
||||
sqlSemanticDiagnosticsEnabled: "Diagnóstico semántico de SQL",
|
||||
sqlSemanticDiagnosticsEnabledDescription: "Al activarse, el editor informa de problemas semánticos como tablas y columnas desconocidas. Desactívalo para reducir el análisis y las comprobaciones de metadatos en SQL grandes.",
|
||||
confirmDangerousSqlExecution: "Confirmar antes de operaciones peligrosas",
|
||||
|
|
|
|||
|
|
@ -5064,6 +5064,11 @@ 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",
|
||||
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",
|
||||
completionTriggerModeRequirePrefix: "Prefisso identificatore digitato",
|
||||
completionTriggerModePositional: "Posizione intelligente",
|
||||
sqlSemanticDiagnosticsEnabled: "Diagnostica semantica SQL",
|
||||
sqlSemanticDiagnosticsEnabledDescription: "Se abilitata, l'editor segnala problemi semantici come tabelle e colonne sconosciute. Disabilitala per ridurre l'analisi e i controlli dei metadati per file SQL di grandi dimensioni.",
|
||||
confirmDangerousSqlExecution: "Conferma prima delle operazioni pericolose",
|
||||
|
|
|
|||
|
|
@ -5438,6 +5438,11 @@ export default withEnglishFallback({
|
|||
syncSnippetGuide: "設定ガイド",
|
||||
insertSpaceAfterCompletion: "補全後に自動でスペースを挿入",
|
||||
insertSpaceAfterCompletionDescription: "キーワード、テーブル名、列名の補全確定時、次の文字が許容する場合に自動でスペースを補填します",
|
||||
completionTriggerMode: "自動補完トリガーモード",
|
||||
completionTriggerModeDescription: "SQL補完ポップアップが自動的に開くタイミングを制御します。手動はショートカットキーが必要; 識別子入力後は1文字以上の入力が必要; スマート位置は現在の動作を維持します。",
|
||||
completionTriggerModeManual: "手動のみ",
|
||||
completionTriggerModeRequirePrefix: "識別子入力後",
|
||||
completionTriggerModePositional: "スマート位置",
|
||||
},
|
||||
driverStore: {
|
||||
jreDirRemoveFailed: "古い JRE ディレクトリを削除できませんでした: {path}(元のエラー: {error})",
|
||||
|
|
|
|||
|
|
@ -4839,6 +4839,11 @@ export default withEnglishFallback({
|
|||
autoCloseBracketsDescription: "여는 괄호나 따옴표를 입력할 때 닫는 괄호와 따옴표를 자동으로 삽입합니다",
|
||||
insertSpaceAfterCompletion: "완성 후 공백 삽입",
|
||||
insertSpaceAfterCompletionDescription: "다음 문자가 허용할 때 키워드, 테이블 또는 컬럼 완성을 수락한 후 공백을 추가합니다",
|
||||
completionTriggerMode: "자동 완성 트리거 모드",
|
||||
completionTriggerModeDescription: "SQL 완성 팝업이 자동으로 열리는 시기를 제어합니다. 수동은 단축키 필요; 입력 식별자는 하나 이상의 문자 입력 필요; 스마트 위치는 현재 동작 유지.",
|
||||
completionTriggerModeManual: "수동만",
|
||||
completionTriggerModeRequirePrefix: "식별자 입력 후",
|
||||
completionTriggerModePositional: "스마트 위치",
|
||||
sqlSemanticDiagnosticsEnabled: "SQL 의미 진단",
|
||||
sqlSemanticDiagnosticsEnabledDescription: "활성화하면 편집기가 알 수 없는 테이블과 컬럼 같은 의미 문제를 보고합니다. 대규모 SQL의 파싱과 메타데이터 검사를 줄이려면 비활성화하세요.",
|
||||
confirmDangerousSqlExecution: "위험한 작업 전에 확인",
|
||||
|
|
|
|||
|
|
@ -5066,6 +5066,11 @@ 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",
|
||||
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",
|
||||
completionTriggerModeRequirePrefix: "Prefixo de identificador digitado",
|
||||
completionTriggerModePositional: "Posição inteligente",
|
||||
sqlSemanticDiagnosticsEnabled: "Diagnóstico semântico de SQL",
|
||||
sqlSemanticDiagnosticsEnabledDescription: "Quando ativado, o editor relata problemas semânticos como tabelas e colunas desconhecidas. Desative para reduzir a análise e verificações de metadados em SQL grande.",
|
||||
confirmDangerousSqlExecution: "Confirmar antes de operações perigosas",
|
||||
|
|
|
|||
|
|
@ -5331,6 +5331,11 @@ export default withEnglishFallback({
|
|||
autoCloseBracketsDescription: "输入左括号或左引号时自动补全对应的右括号或右引号",
|
||||
insertSpaceAfterCompletion: "补全后自动添加空格",
|
||||
insertSpaceAfterCompletionDescription: "选择关键字、表名或列名补全后,在后续字符允许时自动追加空格",
|
||||
completionTriggerMode: "自动补全触发方式",
|
||||
completionTriggerModeDescription: "控制 SQL 补全弹窗何时自动打开。仅手动提示需快捷键;输入标识符后提示需输入至少一个字符;智能位置提示保持当前行为。",
|
||||
completionTriggerModeManual: "仅手动提示",
|
||||
completionTriggerModeRequirePrefix: "输入标识符后提示",
|
||||
completionTriggerModePositional: "智能位置提示",
|
||||
sqlSemanticDiagnosticsEnabled: "SQL 语义诊断",
|
||||
sqlSemanticDiagnosticsEnabledDescription: "开启后,编辑器会提示未知表、字段等语义问题;关闭可减少 SQL 解析和元数据检查的性能开销。",
|
||||
confirmDangerousSqlExecution: "执行危险操作前弹出确认",
|
||||
|
|
|
|||
|
|
@ -4510,6 +4510,11 @@ export default withEnglishFallback({
|
|||
vimModeDescription: "在 SQL 編輯器中使用 Vim 風格的模態編輯",
|
||||
autoCloseBrackets: "自動成對補全",
|
||||
autoCloseBracketsDescription: "輸入左括號或左引號時自動補全對應的右括號或右引號",
|
||||
completionTriggerMode: "自動補全觸發方式",
|
||||
completionTriggerModeDescription: "控制 SQL 補全彈窗何時自動開啟。僅手動提示需快捷鍵;輸入識別碼後提示需輸入至少一個字元;智慧位置提示保持目前行為。",
|
||||
completionTriggerModeManual: "僅手動提示",
|
||||
completionTriggerModeRequirePrefix: "輸入識別碼後提示",
|
||||
completionTriggerModePositional: "智慧位置提示",
|
||||
sqlSemanticDiagnosticsEnabled: "SQL 語意診斷",
|
||||
sqlSemanticDiagnosticsEnabledDescription: "開啟後,編輯器會提示未知資料表、欄位等語意問題;關閉可減少 SQL 解析和中繼資料檢查的效能負擔。",
|
||||
confirmDangerousSqlExecution: "執行危險操作前彈出確認",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||
import { buildSelectStarExpansion, buildSqlCompletionItems, getSqlCompletionContext, selectStarResultColumnsMatch, shouldAutoOpenSqlCompletion } from "@/lib/sql/sqlCompletion";
|
||||
import { sqlCompletionContextFromSemantic } from "@/lib/sql/semantic/completion";
|
||||
import { buildSqlSemanticModel } from "@/lib/sql/semantic/model";
|
||||
import { originForSqlCompletionProvider, originForTypedSqlCompletionStart, shouldAllowSqlCompletionTrigger, type SqlCompletionTriggerFacts } from "@/lib/sql/sqlCompletionTriggerPolicy";
|
||||
|
||||
describe("sqlCompletion keyword snippets", () => {
|
||||
it("auto-opens and suggests SELECT when typing sel", () => {
|
||||
|
|
@ -700,3 +701,109 @@ describe("sqlCompletion scoped metadata ranking", () => {
|
|||
expect(sqlServerItems).toEqual([expect.objectContaining({ label: "Orders", apply: "Orders" })]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldAllowSqlCompletionTrigger", () => {
|
||||
const typingFacts = (overrides: Partial<SqlCompletionTriggerFacts> = {}): SqlCompletionTriggerFacts => ({
|
||||
origin: "typing",
|
||||
hasIdentifierPrefix: false,
|
||||
qualifierTriggered: false,
|
||||
useDatabasePrefix: null,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const explicitFacts = (overrides: Partial<SqlCompletionTriggerFacts> = {}): SqlCompletionTriggerFacts => ({
|
||||
origin: "explicit",
|
||||
hasIdentifierPrefix: false,
|
||||
qualifierTriggered: false,
|
||||
useDatabasePrefix: null,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("explicit", () => {
|
||||
it("allows explicit completion in any mode", () => {
|
||||
expect(shouldAllowSqlCompletionTrigger("manual", explicitFacts())).toBe(true);
|
||||
expect(shouldAllowSqlCompletionTrigger("require-prefix", explicitFacts())).toBe(true);
|
||||
expect(shouldAllowSqlCompletionTrigger("positional", explicitFacts())).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("manual", () => {
|
||||
it("rejects all typing completions", () => {
|
||||
expect(shouldAllowSqlCompletionTrigger("manual", typingFacts())).toBe(false);
|
||||
expect(shouldAllowSqlCompletionTrigger("manual", typingFacts({ hasIdentifierPrefix: true }))).toBe(false);
|
||||
expect(shouldAllowSqlCompletionTrigger("manual", typingFacts({ qualifierTriggered: true }))).toBe(false);
|
||||
expect(shouldAllowSqlCompletionTrigger("manual", typingFacts({ useDatabasePrefix: "m" }))).toBe(false);
|
||||
expect(shouldAllowSqlCompletionTrigger("manual", typingFacts({ positionalEligible: true }))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("require-prefix", () => {
|
||||
it("allows when identifier prefix is non-empty", () => {
|
||||
expect(shouldAllowSqlCompletionTrigger("require-prefix", typingFacts({ hasIdentifierPrefix: true }))).toBe(true);
|
||||
});
|
||||
|
||||
it("allows when qualifier is triggered (dot with qualifier)", () => {
|
||||
expect(shouldAllowSqlCompletionTrigger("require-prefix", typingFacts({ qualifierTriggered: true }))).toBe(true);
|
||||
});
|
||||
|
||||
it("allows when useDatabasePrefix is non-empty", () => {
|
||||
expect(shouldAllowSqlCompletionTrigger("require-prefix", typingFacts({ useDatabasePrefix: "m" }))).toBe(true);
|
||||
expect(shouldAllowSqlCompletionTrigger("require-prefix", typingFacts({ useDatabasePrefix: "Bar" }))).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects empty prefix, no qualifier, no useDatabasePrefix", () => {
|
||||
expect(shouldAllowSqlCompletionTrigger("require-prefix", typingFacts())).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects empty useDatabasePrefix (USE<space> without prefix)", () => {
|
||||
expect(shouldAllowSqlCompletionTrigger("require-prefix", typingFacts({ useDatabasePrefix: "" }))).toBe(false);
|
||||
});
|
||||
|
||||
it("does not use positionalEligible", () => {
|
||||
// Even if positionalEligible is true, require-prefix ignores it.
|
||||
expect(shouldAllowSqlCompletionTrigger("require-prefix", typingFacts({ positionalEligible: true }))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("positional", () => {
|
||||
it("allows when positionalEligible is true", () => {
|
||||
expect(shouldAllowSqlCompletionTrigger("positional", typingFacts({ positionalEligible: true }))).toBe(true);
|
||||
});
|
||||
|
||||
it("allows when useDatabasePrefix is set (even empty)", () => {
|
||||
expect(shouldAllowSqlCompletionTrigger("positional", typingFacts({ useDatabasePrefix: "" }))).toBe(true);
|
||||
expect(shouldAllowSqlCompletionTrigger("positional", typingFacts({ useDatabasePrefix: "m" }))).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects when positionalEligible is false and no useDatabasePrefix", () => {
|
||||
expect(shouldAllowSqlCompletionTrigger("positional", typingFacts({ positionalEligible: false }))).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects when positionalEligible is undefined and no useDatabasePrefix", () => {
|
||||
expect(shouldAllowSqlCompletionTrigger("positional", typingFacts())).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("originForTypedSqlCompletionStart", () => {
|
||||
it("starts a new automatic session as typing", () => {
|
||||
expect(originForTypedSqlCompletionStart(null)).toBe("typing");
|
||||
});
|
||||
|
||||
it("preserves the origin of an active completion session", () => {
|
||||
expect(originForTypedSqlCompletionStart("typing")).toBe("typing");
|
||||
expect(originForTypedSqlCompletionStart("explicit")).toBe("explicit");
|
||||
});
|
||||
});
|
||||
|
||||
describe("originForSqlCompletionProvider", () => {
|
||||
it("classifies an unmarked provider call from CodeMirror", () => {
|
||||
expect(originForSqlCompletionProvider(null, false)).toBe("typing");
|
||||
expect(originForSqlCompletionProvider(null, true)).toBe("explicit");
|
||||
});
|
||||
|
||||
it("preserves the active session independently of the current provider flag", () => {
|
||||
expect(originForSqlCompletionProvider("typing", true)).toBe("typing");
|
||||
expect(originForSqlCompletionProvider("explicit", false)).toBe("explicit");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -49,6 +49,10 @@ describe("EDITOR_SETTINGS_DRAFT_KEYS", () => {
|
|||
it("includes the data-tab reuse mode", () => {
|
||||
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("dataTabReuseMode");
|
||||
});
|
||||
|
||||
it("includes completionTriggerMode", () => {
|
||||
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("completionTriggerMode");
|
||||
});
|
||||
});
|
||||
|
||||
describe("editorSettingsDraftFromSettings", () => {
|
||||
|
|
@ -71,6 +75,16 @@ describe("editorSettingsDraftFromSettings", () => {
|
|||
it("maps the saved SQL open target mode", () => {
|
||||
expect(editorSettingsDraftFromSettings(makeSettings({ savedSqlOpenTargetMode: "current" })).savedSqlOpenTargetMode).toBe("current");
|
||||
});
|
||||
|
||||
it("maps completionTriggerMode from settings", () => {
|
||||
const draft = editorSettingsDraftFromSettings(makeSettings({ completionTriggerMode: "require-prefix" } as Partial<EditorSettings>));
|
||||
expect(draft.completionTriggerMode).toBe("require-prefix");
|
||||
});
|
||||
|
||||
it("normalizes invalid completionTriggerMode to positional", () => {
|
||||
const draft = editorSettingsDraftFromSettings(makeSettings({ completionTriggerMode: "always" as unknown } as Partial<EditorSettings>));
|
||||
expect(draft.completionTriggerMode).toBe("positional");
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeTableOpenPageSizeDraft", () => {
|
||||
|
|
@ -127,6 +141,21 @@ describe("editorSettingsDraftChanged", () => {
|
|||
draft.dataTabReuseMode = "active-tab";
|
||||
expect(editorSettingsDraftChanged(draft, base)).toBe(true);
|
||||
});
|
||||
|
||||
it("detects completionTriggerMode change", () => {
|
||||
const settings = makeSettings({ completionTriggerMode: "positional" } as Partial<EditorSettings>);
|
||||
const draft = editorSettingsDraftFromSettings(settings);
|
||||
const base = editorSettingsDraftFromSettings(settings);
|
||||
draft.completionTriggerMode = "manual";
|
||||
expect(editorSettingsDraftChanged(draft, base)).toBe(true);
|
||||
});
|
||||
|
||||
it("detects no change when completionTriggerMode matches", () => {
|
||||
const settings = makeSettings({ completionTriggerMode: "require-prefix" } as Partial<EditorSettings>);
|
||||
const draft = editorSettingsDraftFromSettings(settings);
|
||||
const base = editorSettingsDraftFromSettings(settings);
|
||||
expect(editorSettingsDraftChanged(draft, base)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("editorSettingsPatchFromDraft", () => {
|
||||
|
|
@ -170,6 +199,23 @@ describe("editorSettingsPatchFromDraft", () => {
|
|||
draft.dataTabReuseMode = "always-new";
|
||||
expect(editorSettingsPatchFromDraft(draft, base).dataTabReuseMode).toBe("always-new");
|
||||
});
|
||||
|
||||
it("includes completionTriggerMode in patch when changed", () => {
|
||||
const settings = makeSettings({ completionTriggerMode: "positional" } as Partial<EditorSettings>);
|
||||
const draft = editorSettingsDraftFromSettings(settings);
|
||||
const base = editorSettingsDraftFromSettings(settings);
|
||||
draft.completionTriggerMode = "manual";
|
||||
const patch = editorSettingsPatchFromDraft(draft, base);
|
||||
expect(patch.completionTriggerMode).toBe("manual");
|
||||
});
|
||||
|
||||
it("omits completionTriggerMode when unchanged", () => {
|
||||
const settings = makeSettings({ completionTriggerMode: "require-prefix" } as Partial<EditorSettings>);
|
||||
const draft = editorSettingsDraftFromSettings(settings);
|
||||
const base = editorSettingsDraftFromSettings(settings);
|
||||
const patch = editorSettingsPatchFromDraft(draft, base);
|
||||
expect(patch.completionTriggerMode).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("EDITOR_SETTINGS_DRAFT_KEYS - tabLayout", () => {
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ describe("settings search", () => {
|
|||
{ titleKey: "toolbar.theme", category: "appearance", targetId: "appearance" },
|
||||
{ titleKey: "settings.sidebarObjectInfoMode", category: "navigation", targetId: "navigation" },
|
||||
{ titleKey: "settings.insertSpaceAfterCompletion", category: "editor", targetId: "editor" },
|
||||
{ titleKey: "settings.completionTriggerMode", category: "editor", targetId: "editor" },
|
||||
{ titleKey: "settings.autoAliasTables", category: "editor", targetId: "editor" },
|
||||
{ titleKey: "settings.clickTableNavigationTarget", category: "editor", targetId: "editor" },
|
||||
{ titleKey: "settings.sqlFormatterKeywordCase", category: "formatter", targetId: "formatter" },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { EditorSettings } from "@/stores/settingsStore";
|
||||
import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize";
|
||||
import { normalizeCompletionTriggerMode } from "@/lib/sql/sqlCompletionTriggerPolicy";
|
||||
|
||||
export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
||||
"fontFamily",
|
||||
|
|
@ -66,6 +67,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
|||
"sqlVariableSyntaxOverrides",
|
||||
"continueOnErrorOnBatch",
|
||||
"clickTableNavigationTarget",
|
||||
"completionTriggerMode",
|
||||
] as const satisfies readonly (keyof EditorSettings)[];
|
||||
|
||||
export type EditorSettingsDraftKey = (typeof EDITOR_SETTINGS_DRAFT_KEYS)[number];
|
||||
|
|
@ -83,6 +85,7 @@ export function normalizeTableOpenPageSizeDraft(value: unknown): number {
|
|||
|
||||
function normalizedDraftValue(key: EditorSettingsDraftKey, value: unknown): unknown {
|
||||
if (key === "tableOpenPageSize") return normalizeTableOpenPageSizeDraft(value);
|
||||
if (key === "completionTriggerMode") return normalizeCompletionTriggerMode(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -114,6 +114,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-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" },
|
||||
{ id: "editor-prefill-query", category: "editor", titleKey: "settings.prefillNewQueryWithSelect", descriptionKey: "settings.prefillNewQueryWithSelectDescription", targetId: "editor" },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
export type SqlCompletionTriggerMode = "manual" | "require-prefix" | "positional";
|
||||
export type SqlCompletionTriggerOrigin = "typing" | "explicit";
|
||||
|
||||
export const SQL_COMPLETION_TRIGGER_MODES: readonly SqlCompletionTriggerMode[] = ["manual", "require-prefix", "positional"];
|
||||
|
||||
export function originForTypedSqlCompletionStart(activeOrigin: SqlCompletionTriggerOrigin | null): SqlCompletionTriggerOrigin {
|
||||
// Programmatic refreshes can run while a shortcut-opened session is active.
|
||||
// Preserve that explicit origin so typing and metadata refreshes do not make
|
||||
// the session subject to automatic-trigger mode gates.
|
||||
return activeOrigin ?? "typing";
|
||||
}
|
||||
|
||||
export function originForSqlCompletionProvider(activeOrigin: SqlCompletionTriggerOrigin | null, explicit: boolean): SqlCompletionTriggerOrigin {
|
||||
// Programmatic starts set their origin before calling CodeMirror, while
|
||||
// activate-on-typing calls the provider with explicit=false. Therefore an
|
||||
// otherwise unmarked explicit call is always the manual shortcut session.
|
||||
return activeOrigin ?? (explicit ? "explicit" : "typing");
|
||||
}
|
||||
|
||||
export function normalizeCompletionTriggerMode(value: unknown): SqlCompletionTriggerMode {
|
||||
if (typeof value === "string" && (value === "manual" || value === "require-prefix" || value === "positional")) {
|
||||
return value;
|
||||
}
|
||||
return "positional";
|
||||
}
|
||||
|
||||
export interface SqlCompletionTriggerFacts {
|
||||
/** typing (DBX programmatic start / implicit) vs explicit (manual completion shortcut) */
|
||||
origin: SqlCompletionTriggerOrigin;
|
||||
/** completion context context.prefix.length > 0 (not trailing '.') */
|
||||
hasIdentifierPrefix: boolean;
|
||||
/** previousChar === "." && context.qualifier != null */
|
||||
qualifierTriggered: boolean;
|
||||
/** resolveSqlServerUseDatabaseCompletion().prefix ('m' / 'Bar' / '') */
|
||||
useDatabasePrefix: string | null;
|
||||
/** Only computed for positional mode: existing shouldAutoOpenSqlCompletion(sql, cursor, options) result */
|
||||
positionalEligible?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure trigger policy decision (the single gate, without sql/position itself).
|
||||
*
|
||||
* Decision rules (priority order):
|
||||
* 1. suppressed context (comment / string literal) -> reject first (including explicit)
|
||||
* 2. explicit -> allow directly (manual shortcut available in any mode)
|
||||
* 3. manual -> false (return before any semantic model / metadata read)
|
||||
* 4. require-prefix -> hasIdentifierPrefix || qualifierTriggered || (useDatabasePrefix != null && useDatabasePrefix.length > 0)
|
||||
* 5. positional -> positionalEligible || useDatabasePrefix != null
|
||||
*/
|
||||
export function shouldAllowSqlCompletionTrigger(mode: SqlCompletionTriggerMode, facts: SqlCompletionTriggerFacts): boolean {
|
||||
// Suppressed context (comment / string literal) is checked by the caller before
|
||||
// constructing facts, so this function treats the caller as already gate-kept.
|
||||
// However, for safety, explicit still bypasses mode checks.
|
||||
if (facts.origin === "explicit") return true;
|
||||
if (mode === "manual") return false;
|
||||
if (mode === "require-prefix") {
|
||||
return facts.hasIdentifierPrefix || facts.qualifierTriggered || (facts.useDatabasePrefix != null && facts.useDatabasePrefix.length > 0);
|
||||
}
|
||||
// positional
|
||||
return (facts.positionalEligible ?? false) || facts.useDatabasePrefix != null;
|
||||
}
|
||||
|
|
@ -351,6 +351,26 @@ describe("normalizeEditorSettings - clickTableNavigationTarget", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("normalizeEditorSettings - completionTriggerMode", () => {
|
||||
it("defaults completionTriggerMode to positional", () => {
|
||||
expect(normalizeEditorSettings({}).completionTriggerMode).toBe("positional");
|
||||
});
|
||||
|
||||
it("preserves the three valid modes", () => {
|
||||
expect(normalizeEditorSettings({ completionTriggerMode: "manual" }).completionTriggerMode).toBe("manual");
|
||||
expect(normalizeEditorSettings({ completionTriggerMode: "require-prefix" }).completionTriggerMode).toBe("require-prefix");
|
||||
expect(normalizeEditorSettings({ completionTriggerMode: "positional" }).completionTriggerMode).toBe("positional");
|
||||
});
|
||||
|
||||
it("normalizes invalid values to positional", () => {
|
||||
expect(normalizeEditorSettings({ completionTriggerMode: "always" } as any).completionTriggerMode).toBe("positional");
|
||||
expect(normalizeEditorSettings({ completionTriggerMode: "" } as any).completionTriggerMode).toBe("positional");
|
||||
expect(normalizeEditorSettings({ completionTriggerMode: undefined } as any).completionTriggerMode).toBe("positional");
|
||||
expect(normalizeEditorSettings({ completionTriggerMode: null } as any).completionTriggerMode).toBe("positional");
|
||||
expect(normalizeEditorSettings({ completionTriggerMode: 123 } as any).completionTriggerMode).toBe("positional");
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeEditorSettings - tabLayout", () => {
|
||||
it("defaults tabLayout to scroll", () => {
|
||||
expect(normalizeEditorSettings({}).tabLayout).toBe("scroll");
|
||||
|
|
|
|||
|
|
@ -18,10 +18,11 @@ import { DEFAULT_SQL_FORMATTER_SETTINGS, normalizeSqlFormatterSettings, type Sql
|
|||
import { normalizeSqlVariableSyntaxOverrides, type SqlVariableSyntaxOverrides } from "@/lib/sql/sqlVariableSyntax";
|
||||
import { DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS, normalizeTableColumnTemplateFields } from "@/lib/table/tableColumnTemplates";
|
||||
import { type DataTabReuseMode, DEFAULT_DATA_TAB_REUSE_MODE, normalizeDataTabReuseMode } from "@/lib/tabs/dataTabReuseMode";
|
||||
import { normalizeCompletionTriggerMode, type SqlCompletionTriggerMode } from "@/lib/sql/sqlCompletionTriggerPolicy";
|
||||
import type { AiApiStyle, AiAuthMethod, AiChatSelectionState, AiConfig, AiConfigItem, AiConfiguredModel, AiEffortLevel, AiEffortSelection, AiModelEffortPreference, AiProvider, AiReasoningLevel, AiTestConnectionResult } from "@/types/ai";
|
||||
import type { SqlSnippet, TableInfoTab } from "@/types/database";
|
||||
|
||||
export type { AiApiStyle, AiAuthMethod, AiChatSelectionState, AiConfig, AiConfigItem, AiConfiguredModel, AiEffortLevel, AiEffortSelection, AiProvider, AiReasoningLevel, AiTestConnectionResult, DataTabReuseMode, SavedSqlOpenTargetMode };
|
||||
export type { AiApiStyle, AiAuthMethod, AiChatSelectionState, AiConfig, AiConfigItem, AiConfiguredModel, AiEffortLevel, AiEffortSelection, AiProvider, AiReasoningLevel, AiTestConnectionResult, DataTabReuseMode, SavedSqlOpenTargetMode, SqlCompletionTriggerMode };
|
||||
|
||||
export interface DesktopSettings {
|
||||
show_tray_icon: boolean;
|
||||
|
|
@ -550,6 +551,7 @@ export interface EditorSettings {
|
|||
sqlVariableSyntaxOverrides: SqlVariableSyntaxOverrides;
|
||||
continueOnErrorOnBatch: boolean;
|
||||
clickTableNavigationTarget: ClickTableNavigationTarget;
|
||||
completionTriggerMode: SqlCompletionTriggerMode;
|
||||
}
|
||||
|
||||
export interface ToolbarItems {
|
||||
|
|
@ -730,6 +732,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
|||
sqlVariableSyntaxOverrides: {},
|
||||
continueOnErrorOnBatch: false,
|
||||
clickTableNavigationTarget: "data",
|
||||
completionTriggerMode: "positional",
|
||||
};
|
||||
|
||||
export const STORAGE_KEY = "dbx-editor-settings";
|
||||
|
|
@ -1079,6 +1082,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
|||
sqlVariableSyntaxOverrides: normalizeSqlVariableSyntaxOverrides(settings.sqlVariableSyntaxOverrides),
|
||||
continueOnErrorOnBatch: settings.continueOnErrorOnBatch === true,
|
||||
clickTableNavigationTarget: normalizeClickTableNavigationTarget(settings.clickTableNavigationTarget),
|
||||
completionTriggerMode: normalizeCompletionTriggerMode(settings.completionTriggerMode),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1536,6 +1540,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
|||
if (partial.sqlVariableSyntaxOverrides !== undefined) editorSettings.value.sqlVariableSyntaxOverrides = normalizeSqlVariableSyntaxOverrides(partial.sqlVariableSyntaxOverrides);
|
||||
if (partial.continueOnErrorOnBatch !== undefined) editorSettings.value.continueOnErrorOnBatch = partial.continueOnErrorOnBatch === true;
|
||||
if (partial.clickTableNavigationTarget !== undefined) editorSettings.value.clickTableNavigationTarget = normalizeClickTableNavigationTarget(partial.clickTableNavigationTarget);
|
||||
if (partial.completionTriggerMode !== undefined) editorSettings.value.completionTriggerMode = normalizeCompletionTriggerMode(partial.completionTriggerMode);
|
||||
saveEditorSettings(editorSettings.value);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue