feat(editor): show column hints for INSERT values

This commit is contained in:
Freedom authored and GitHub committed 2026-07-13 00:39:36 +08:00
1 parent ff570d5268
commit 86dad5db9f
18 files changed
+1076 -5

No files matched your search

@@ -84,7 +84,7 @@ import { eventToShortcut } from "@/lib/editor/keyboardShortcuts";
import { SHORTCUT_DEFINITIONS, findShortcutConflict, normalizeShortcutSettings, type ShortcutActionId } from "@/lib/editor/shortcutRegistry";
import { formatShortcutDisplay } from "@/lib/editor/shortcutDisplay";
import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebar/sidebarTableNameDisplay";
import { currentStatementFrameRangeTo, visualSqlColumns } from "@/lib/sql/currentStatementFrame";
import { currentStatementFrameRangeTo, visualSqlColumnsWithInlineHints } from "@/lib/sql/currentStatementFrame";
import { normalizeSqlFormatterSettings, type SqlFormatterSettings } from "@/lib/sql/sqlFormatterConfig";
import { currentExecutableStatementRange, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
import { executableStatementRangeCacheForDoc, executableStatementRangeStartingAt, type ExecutableStatementRangeCache } from "@/lib/sql/executableStatementRangeCache";
@@ -255,6 +255,7 @@ const editExecuteMode = ref(settingsStore.editorSettings.executeMode);
const editShowExecutionTargetPicker = ref(settingsStore.editorSettings.showExecutionTargetPicker);
const editShowStatementRunButtons = ref(settingsStore.editorSettings.showStatementRunButtons);
const editShowCurrentStatementFrame = ref(settingsStore.editorSettings.showCurrentStatementFrame);
const editShowInsertValueHints = ref(settingsStore.editorSettings.showInsertValueHints);
const editAutoAliasTables = ref(settingsStore.editorSettings.autoAliasTables);
const editWordWrap = ref(settingsStore.editorSettings.wordWrap);
const editVimModeEnabled = ref(settingsStore.editorSettings.vimModeEnabled);
@@ -388,6 +389,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
showExecutionTargetPicker: editShowExecutionTargetPicker.value,
showStatementRunButtons: editShowStatementRunButtons.value,
showCurrentStatementFrame: editShowCurrentStatementFrame.value,
showInsertValueHints: editShowInsertValueHints.value,
autoAliasTables: editAutoAliasTables.value,
wordWrap: editWordWrap.value,
vimModeEnabled: editVimModeEnabled.value,
@@ -660,6 +662,7 @@ function syncEditorSettingsDraftFromStore() {
editShowExecutionTargetPicker.value = settingsStore.editorSettings.showExecutionTargetPicker;
editShowStatementRunButtons.value = settingsStore.editorSettings.showStatementRunButtons;
editShowCurrentStatementFrame.value = settingsStore.editorSettings.showCurrentStatementFrame;
editShowInsertValueHints.value = settingsStore.editorSettings.showInsertValueHints;
editAutoAliasTables.value = settingsStore.editorSettings.autoAliasTables;
editWordWrap.value = settingsStore.editorSettings.wordWrap;
editVimModeEnabled.value = settingsStore.editorSettings.vimModeEnabled;
@@ -845,6 +848,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker;
editShowStatementRunButtons.value = DEFAULT_EDITOR_SETTINGS.showStatementRunButtons;
editShowCurrentStatementFrame.value = DEFAULT_EDITOR_SETTINGS.showCurrentStatementFrame;
editShowInsertValueHints.value = DEFAULT_EDITOR_SETTINGS.showInsertValueHints;
editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables;
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
editVimModeEnabled.value = DEFAULT_EDITOR_SETTINGS.vimModeEnabled;
@@ -919,6 +923,7 @@ function resetAllDefaults() {
editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker;
editShowStatementRunButtons.value = DEFAULT_EDITOR_SETTINGS.showStatementRunButtons;
editShowCurrentStatementFrame.value = DEFAULT_EDITOR_SETTINGS.showCurrentStatementFrame;
editShowInsertValueHints.value = DEFAULT_EDITOR_SETTINGS.showInsertValueHints;
editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables;
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
editVimModeEnabled.value = DEFAULT_EDITOR_SETTINGS.vimModeEnabled;
@@ -2398,7 +2403,7 @@ function buildPreviewCurrentStatementFrameExtension(viewModule: Pick<typeof impo
for (let lineNumber = startLine.number; lineNumber <= endLine.number; lineNumber += 1) {
const line = view.state.doc.line(lineNumber);
const lineRangeTo = Math.min(line.to, frameTo);
maxWidth = Math.max(maxWidth, visualSqlColumns(view.state.doc.sliceString(line.from, lineRangeTo)));
maxWidth = Math.max(maxWidth, visualSqlColumnsWithInlineHints(view.state.doc.sliceString(line.from, lineRangeTo), line.from, lineRangeTo));
}
const deco: any[] = [];
@@ -2723,6 +2728,14 @@ onUnmounted(cleanupPreviewEditor);
<Switch id="editor-show-current-statement-frame" v-model="editShowCurrentStatementFrame" 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-show-insert-value-hints">{{ t("settings.showInsertValueHints") }}</Label>
<p class="text-xs text-muted-foreground">{{ t("settings.showInsertValueHintsDescription") }}</p>
</div>
<Switch id="editor-show-insert-value-hints" v-model="editShowInsertValueHints" 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-word-wrap">{{ t("settings.wordWrap") }}</Label>
@@ -12,7 +12,8 @@ import { copyToClipboard, readTextFromClipboard } from "@/lib/common/clipboard";
import { resolveExecutableSql, type SqlExecutionSnapshot, type SqlExecutionOverride, type SqlExecutionCandidate } from "@/lib/sql/sqlExecutionTarget";
import { buildExecutionCandidates, hasMultipleExecutionTargets, supportsExecutionTargetPicker, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
import { executableStatementRangeAtCursor, executableStatementRangeCacheForDoc, executableStatementRangeStartingAt as executableStatementRangeStartingAtLine, type ExecutableStatementRangeCache } from "@/lib/sql/executableStatementRangeCache";
import { currentStatementFrameRangeTo, visualSqlColumns } from "@/lib/sql/currentStatementFrame";
import { currentStatementFrameRangeTo, visualSqlColumnsWithInlineHints } from "@/lib/sql/currentStatementFrame";
import { parseInsertValueHints } from "@/lib/sql/insertValueHints";
import { formatSqlText, type SqlFormatDialect } from "@/lib/sql/sqlFormatter";
import { buildSqlInConditionFromPasteSource, insertTextForSqlInCondition } from "@/lib/sql/sqlInListPaste";
import { resolveSqlSingleQuoteKeyAction } from "@/lib/sql/sqlQuoteCaret";
@@ -56,6 +57,7 @@ import { clampEditorFontSize, createEditorZoomCommitScheduler, fontSizeFromGestu
import { normalizeShortcutSettings, shortcutToCodeMirrorKey } from "@/lib/editor/shortcutRegistry";
import { trimmedSelectionLayer } from "@/lib/editor/codemirrorTrimmedSelectionLayer";
import { selectionMatchOccurrences } from "@/lib/editor/codemirrorSelectionMatches";
import { createInsertValueHintsExtension, requestInsertValueHintsRefresh } from "@/lib/editor/codemirrorInsertValueHints";
import { createDbxCodeMirrorSqlDialect } from "@/lib/editor/codemirrorSqlDialect";
import { startsQueryEditorRectangularSelection } from "@/lib/editor/queryEditorPointerSelection";
import { isSchemaAware, isSingleDatabase, supportsSqlInListPaste } from "@/lib/database/databaseFeatureSupport";
@@ -296,6 +298,7 @@ const queryEditorAppearanceSettings = computed(() => {
vimModeEnabled: settings.vimModeEnabled,
autoCloseBrackets: settings.autoCloseBrackets,
showCurrentStatementFrame: settings.showCurrentStatementFrame,
showInsertValueHints: settings.showInsertValueHints,
shortcuts: settings.shortcuts,
showStatementRunButtons: settings.showStatementRunButtons,
};
@@ -1098,6 +1101,51 @@ function completionCacheKey(table: { name: string; schema?: string | null }) {
return schema ? `${schema}.${table.name}` : table.name;
}
const pendingInsertValueHintColumnLoads = new Set<string>();
function insertHintCacheKey(table: { name: string; schema?: string | null; database?: string | null }) {
if (table.database) {
return table.schema ? `${table.database}.${table.schema}.${table.name}` : `${table.database}.${table.name}`;
}
return completionCacheKey(table);
}
function insertHintMetadataTarget(table: { name: string; schema?: string | null; database?: string | null }): { database: string; schema?: string } | null {
if (props.database == null) return null;
if (table.database) {
return { database: table.database, schema: table.schema ?? undefined };
}
return completionMetadataTarget(table);
}
function getInsertValueHintTableColumns(table: string, schema?: string, database?: string): string[] | undefined {
const cacheKey = insertHintCacheKey({ name: table, schema, database });
const cached = cachedColumnsByTable.get(cacheKey);
if (!cached) return undefined;
return cached.map((column) => column.name);
}
function requestInsertValueHintTableColumns(table: string, schema?: string, database?: string) {
if (!props.connectionId || props.database == null) return;
if (props.databaseType === "redis" || props.databaseType === "mongodb" || props.databaseType === "elasticsearch") return;
const cacheKey = insertHintCacheKey({ name: table, schema, database });
if (cachedColumnsByTable.has(cacheKey) || pendingInsertValueHintColumnLoads.has(cacheKey)) return;
const target = insertHintMetadataTarget({ name: table, schema, database });
if (!target) return;
pendingInsertValueHintColumnLoads.add(cacheKey);
void connectionStore
.listCompletionColumns(props.connectionId, target.database, table, target.schema)
.then((columns) => {
cachedColumnsByTable.set(cacheKey, columns);
loadedColumnsByTable.add(cacheKey.toLowerCase());
if (view.value) requestInsertValueHintsRefresh(view.value);
})
.catch(() => {})
.finally(() => {
pendingInsertValueHintColumnLoads.delete(cacheKey);
});
}
function supportsDatabaseQualifierCompletion(): boolean {
return !!props.databaseType && !isSchemaAware(props.databaseType) && !isSingleDatabase(props.databaseType);
}
@@ -2698,11 +2746,22 @@ onMounted(async () => {
const startLine = view.state.doc.lineAt(range.from);
const frameTo = currentStatementFrameTo(view, range);
const endLine = view.state.doc.lineAt(Math.max(range.from, frameTo - 1));
let insertValueHints: Array<{ from: number; column: string }> = [];
try {
if (settingsStore.editorSettings.showInsertValueHints && props.databaseType !== "redis" && props.databaseType !== "mongodb" && props.databaseType !== "elasticsearch") {
insertValueHints = parseInsertValueHints(view.state.doc.sliceString(range.from, range.to), { resolveTableColumns: getInsertValueHintTableColumns }).map((hint) => ({
...hint,
from: hint.from + range.from,
}));
}
} catch {
insertValueHints = [];
}
let maxWidth = 1;
for (let lineNumber = startLine.number; lineNumber <= endLine.number; lineNumber += 1) {
const line = view.state.doc.line(lineNumber);
const lineRangeTo = Math.min(line.to, frameTo);
maxWidth = Math.max(maxWidth, visualSqlColumns(view.state.doc.sliceString(line.from, lineRangeTo)));
maxWidth = Math.max(maxWidth, visualSqlColumnsWithInlineHints(view.state.doc.sliceString(line.from, lineRangeTo), line.from, lineRangeTo, insertValueHints));
}
const deco: any[] = [];
@@ -2807,6 +2866,11 @@ onMounted(async () => {
hoverTooltip((currentView, pos) => resolveSqlHoverTooltip(currentView, pos)),
buildSqlSignatureExtension(),
diagnosticComp.of(buildSqlDiagnosticExtension()),
createInsertValueHintsExtension({
isEnabled: () => settingsStore.editorSettings.showInsertValueHints && props.databaseType !== "redis" && props.databaseType !== "mongodb" && props.databaseType !== "elasticsearch",
getTableColumns: getInsertValueHintTableColumns,
requestTableColumns: requestInsertValueHintTableColumns,
}),
previewRangeComp.of(buildPreviewRangeExtension()),
Prec.highest(
keymap.of([
@@ -3211,6 +3275,13 @@ watch(
},
);
watch(
() => settingsStore.editorSettings.showInsertValueHints,
() => {
if (view.value) requestInsertValueHintsRefresh(view.value);
},
);
function pauseQueryEditorBackgroundWork() {
flushEditorViewport();
flushEditorSelection();