feat(completion): configurable SQL completion trigger modes
This commit is contained in:
1 parent
bad7f6b472
commit
5b85f9eae8
18 files changed
+457
-18
No files matched your search
@@ -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) => {
|
||||
|
||||
Reference in new issue
Block a user