feat(editor): extend insert column hints to select
This commit is contained in:
1 parent
2e8ed4b485
commit
600af87532
14 files changed
+341
-25
No files matched your search
@@ -129,7 +129,7 @@ import { normalizeShortcutSettings, shortcutToCodeMirrorKey } from "@/lib/editor
|
||||
import { trimmedSelectionLayer } from "@/lib/editor/codemirrorTrimmedSelectionLayer";
|
||||
import { currentStatementFrameLayer } from "@/lib/editor/codemirrorCurrentStatementFrameLayer";
|
||||
import { selectionMatchOccurrences } from "@/lib/editor/codemirrorSelectionMatches";
|
||||
import { createInsertValueHintsExtension, requestInsertValueHintsRefresh } from "@/lib/editor/codemirrorInsertValueHints";
|
||||
import { createInsertValueHintsExtension, requestInsertValueHintsRefresh, supportsInsertValueHints } from "@/lib/editor/codemirrorInsertValueHints";
|
||||
import { sqlBlockFoldService } from "@/lib/editor/codemirrorSqlBlockFolding";
|
||||
import { focusEditorView } from "@/lib/editor/queryEditorFocus";
|
||||
import { createDbxCodeMirrorSqlDialect, type CodeMirrorSqlDialectName } from "@/lib/editor/codemirrorSqlDialect";
|
||||
@@ -727,8 +727,6 @@ const queryEditorAppearanceSettings = computed(() => {
|
||||
wordWrap: settings.wordWrap,
|
||||
vimModeEnabled: settings.vimModeEnabled,
|
||||
autoCloseBrackets: settings.autoCloseBrackets,
|
||||
showCurrentStatementFrame: settings.showCurrentStatementFrame,
|
||||
showInsertValueHints: settings.showInsertValueHints,
|
||||
showLineNumbers: settings.showLineNumbers,
|
||||
shortcuts: settings.shortcuts,
|
||||
showStatementRunButtons: settings.showStatementRunButtons,
|
||||
@@ -2432,7 +2430,7 @@ function getInsertValueHintTableColumns(table: string, schema?: string, database
|
||||
|
||||
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" || props.databaseType === "easysearch" || props.databaseType === "meilisearch" || props.databaseType === "victoriametrics") return;
|
||||
if (!supportsInsertValueHints(props.databaseType)) return;
|
||||
const cacheKey = insertHintCacheKey({ name: table, schema, database });
|
||||
const hasCachedColumns = props.databaseType === "sqlserver" ? cachedInsertValueHintColumnsByTable.has(cacheKey) : cachedColumnsByTable.has(cacheKey);
|
||||
if (hasCachedColumns || pendingInsertValueHintColumnLoads.has(cacheKey)) return;
|
||||
@@ -5768,8 +5766,7 @@ onMounted(async () => {
|
||||
sqlSignatureComp.of(buildSqlSignatureExtension()),
|
||||
diagnosticComp.of(buildSqlDiagnosticExtension()),
|
||||
createInsertValueHintsExtension({
|
||||
isEnabled: () =>
|
||||
settingsStore.editorSettings.showInsertValueHints && props.databaseType !== "redis" && props.databaseType !== "mongodb" && props.databaseType !== "elasticsearch" && props.databaseType !== "easysearch" && props.databaseType !== "meilisearch" && props.databaseType !== "victoriametrics",
|
||||
isEnabled: () => settingsStore.editorSettings.showInsertValueHints && supportsInsertValueHints(props.databaseType),
|
||||
getTableColumns: getInsertValueHintTableColumns,
|
||||
requestTableColumns: requestInsertValueHintTableColumns,
|
||||
getDialectId: () => resolveSqlDialectId({ databaseType: props.databaseType, dialect: sqlBehaviorDialect() }),
|
||||
@@ -6670,7 +6667,7 @@ defineExpose({
|
||||
shouldBlockExecutionShortcut,
|
||||
requestExecute,
|
||||
requestExecuteInNewResultTab,
|
||||
requestPreviewChanges,
|
||||
requestPreviewChanges,
|
||||
captureExecutionSnapshot,
|
||||
pasteClipboardAsSqlInCondition,
|
||||
focusStatementRange,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, watchEffect } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Play, CirclePlay, Loader2, Square, Database, Check, Table2, AlignLeft, GitBranch, Save, FolderOpen, X, Shield, Download, RotateCcw, AlertTriangle, ClipboardPaste, Minimize2, SpellCheck2, Eye } from "@lucide/vue";
|
||||
import { Play, CirclePlay, Loader2, Square, Database, Check, Table2, AlignLeft, GitBranch, Save, FolderOpen, X, Shield, Download, RotateCcw, AlertTriangle, ClipboardPaste, Minimize2, SpellCheck2, BetweenVerticalStart, Eye } from "@lucide/vue";
|
||||
import { supportsInsertValueHints } from "@/lib/editor/codemirrorInsertValueHints";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
||||
@@ -166,6 +167,12 @@ function toggleSqlSemanticDiagnostics() {
|
||||
sqlSemanticDiagnosticsMode: sqlSemanticDiagnosticsEnabled.value ? "disabled" : "enabled",
|
||||
});
|
||||
}
|
||||
const insertValueHintsEnabled = computed(() => settingsStore.editorSettings.showInsertValueHints);
|
||||
const insertValueHintsToggleTooltip = computed(() => (insertValueHintsEnabled.value ? t("toolbar.insertValueHintsToggleOn") : t("toolbar.insertValueHintsToggleOff")));
|
||||
const supportsInsertValueHintsToggle = computed(() => supportsInsertValueHints(props.activeConnection?.db_type));
|
||||
function toggleInsertValueHints() {
|
||||
settingsStore.updateEditorSettings({ showInsertValueHints: !insertValueHintsEnabled.value });
|
||||
}
|
||||
const isTransactionActive = computed(() => !!props.txnSessionId);
|
||||
const isManualTransactionMode = computed(() => props.autoCommit === false || isTransactionActive.value);
|
||||
const transactionModeBadge = computed(() => (isManualTransactionMode.value ? "M" : "A"));
|
||||
@@ -398,6 +405,22 @@ async function changeCatalog(selectedCatalog: string) {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ sqlSemanticDiagnosticsToggleTooltip }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="supportsInsertValueHintsToggle">
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-6 w-6"
|
||||
:class="insertValueHintsEnabled ? 'text-sky-600 bg-sky-500/10 hover:bg-sky-500/20 hover:text-sky-700 dark:text-sky-300 dark:hover:text-sky-200' : 'text-muted-foreground/50 hover:bg-muted hover:text-muted-foreground'"
|
||||
:aria-label="insertValueHintsToggleTooltip"
|
||||
:aria-pressed="insertValueHintsEnabled"
|
||||
@click="toggleInsertValueHints"
|
||||
>
|
||||
<BetweenVerticalStart class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ insertValueHintsToggleTooltip }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="activeConnection?.db_type === 'redis'">
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
|
||||
@@ -160,6 +160,8 @@ export default {
|
||||
keywordCaseUpper: "Use upper-case SQL keywords",
|
||||
sqlSemanticDiagnosticsToggleOn: "SQL semantic diagnostics: On (click to disable)",
|
||||
sqlSemanticDiagnosticsToggleOff: "SQL semantic diagnostics: Off (click to enable)",
|
||||
insertValueHintsToggleOn: "INSERT column hints: On (click to disable)",
|
||||
insertValueHintsToggleOff: "INSERT column hints: Off (click to enable)",
|
||||
autoCommit: "Auto Commit",
|
||||
manualTransaction: "Manual Transaction",
|
||||
autoCommitAgent: "Auto Commit (depends on Agent backend)",
|
||||
@@ -6691,7 +6693,7 @@ export default {
|
||||
showCurrentStatementFrame: "Show current statement frame",
|
||||
showCurrentStatementFrameDescription: "When enabled, the SQL editor draws an outline around the current executable statement; when disabled, the outline is hidden.",
|
||||
showInsertValueHints: "Show INSERT value column hints",
|
||||
showInsertValueHintsDescription: "When enabled, the SQL editor shows column-name hints next to each value in INSERT ... VALUES clauses.",
|
||||
showInsertValueHintsDescription: "When enabled, the SQL editor shows column-name hints next to each value or SELECT projection in INSERT ... VALUES and INSERT ... SELECT clauses.",
|
||||
previewStatementRunButton: "Preview statement run button",
|
||||
wordWrap: "Word wrap",
|
||||
wordWrapDescription: "Wrap long lines within the editor width",
|
||||
|
||||
@@ -162,6 +162,8 @@ export default withEnglishFallback({
|
||||
keywordCaseUpper: "Usar palabras clave SQL en mayúsculas",
|
||||
sqlSemanticDiagnosticsToggleOn: "Diagnóstico semántico de SQL: activado (clic para desactivar)",
|
||||
sqlSemanticDiagnosticsToggleOff: "Diagnóstico semántico de SQL: desactivado (clic para activar)",
|
||||
insertValueHintsToggleOn: "Sugerencias de columnas INSERT: activadas (clic para desactivar)",
|
||||
insertValueHintsToggleOff: "Sugerencias de columnas INSERT: desactivadas (clic para activar)",
|
||||
autoCommit: "Auto Commit",
|
||||
manualTransaction: "Transacción Manual",
|
||||
autoCommitAgent: "Auto Commit (depende del backend del Agente)",
|
||||
@@ -6333,7 +6335,7 @@ export default withEnglishFallback({
|
||||
showCurrentStatementFrame: "Mostrar marco de la sentencia actual",
|
||||
showCurrentStatementFrameDescription: "Al activarlo, el editor SQL dibuja un contorno alrededor de la sentencia ejecutable actual; al desactivarlo, se oculta.",
|
||||
showInsertValueHints: "Mostrar pistas de columnas en VALUES de INSERT",
|
||||
showInsertValueHintsDescription: "Al activarlo, el editor SQL muestra el nombre de columna junto a cada valor en cláusulas INSERT ... VALUES.",
|
||||
showInsertValueHintsDescription: "Al activarlo, el editor SQL muestra el nombre de columna junto a cada valor o proyección SELECT en cláusulas INSERT ... VALUES e INSERT ... SELECT.",
|
||||
previewStatementRunButton: "Botón de ejecución de sentencia en vista previa",
|
||||
wordWrap: "Ajuste de línea",
|
||||
wordWrapDescription: "Ajustar las líneas largas al ancho del editor",
|
||||
|
||||
@@ -161,6 +161,8 @@ export default withEnglishFallback({
|
||||
keywordCaseUpper: "Usa parole chiave SQL maiuscole",
|
||||
sqlSemanticDiagnosticsToggleOn: "Diagnostica semantica SQL: attiva (clic per disattivare)",
|
||||
sqlSemanticDiagnosticsToggleOff: "Diagnostica semantica SQL: disattiva (clic per attivare)",
|
||||
insertValueHintsToggleOn: "Suggerimenti colonne INSERT: attivi (clic per disattivare)",
|
||||
insertValueHintsToggleOff: "Suggerimenti colonne INSERT: disattivi (clic per attivare)",
|
||||
autoCommit: "Auto Commit",
|
||||
manualTransaction: "Transazione Manuale",
|
||||
autoCommitAgent: "Auto Commit (dipende dal backend dell'Agente)",
|
||||
@@ -6333,7 +6335,7 @@ export default withEnglishFallback({
|
||||
showCurrentStatementFrame: "Mostra cornice istruzione corrente",
|
||||
showCurrentStatementFrameDescription: "Se attivo, l'editor SQL disegna un contorno intorno all'istruzione eseguibile corrente; se disattivato, il contorno è nascosto.",
|
||||
showInsertValueHints: "Mostra suggerimenti colonne nei VALUES di INSERT",
|
||||
showInsertValueHintsDescription: "Se attivo, l'editor SQL mostra il nome della colonna accanto a ogni valore nelle clausole INSERT ... VALUES.",
|
||||
showInsertValueHintsDescription: "Se attivo, l'editor SQL mostra il nome della colonna accanto a ogni valore o proiezione SELECT nelle clausole INSERT ... VALUES e INSERT ... SELECT.",
|
||||
previewStatementRunButton: "Pulsante di esecuzione istruzione in anteprima",
|
||||
wordWrap: "A capo automatico",
|
||||
wordWrapDescription: "Manda a capo le righe lunghe entro la larghezza dell'editor",
|
||||
|
||||
@@ -162,6 +162,8 @@ export default withEnglishFallback({
|
||||
keywordCaseUpper: "SQLキーワードを大文字にする",
|
||||
sqlSemanticDiagnosticsToggleOn: "SQLセマンティック診断:オン(クリックでオフ)",
|
||||
sqlSemanticDiagnosticsToggleOff: "SQLセマンティック診断:オフ(クリックでオン)",
|
||||
insertValueHintsToggleOn: "INSERT 列名ヒント:オン(クリックでオフ)",
|
||||
insertValueHintsToggleOff: "INSERT 列名ヒント:オフ(クリックでオン)",
|
||||
autoCommit: "自動コミット",
|
||||
manualTransaction: "手動トランザクション",
|
||||
autoCommitAgent: "自動コミット(Agentバックエンドに依存)",
|
||||
@@ -6359,7 +6361,7 @@ export default withEnglishFallback({
|
||||
showCurrentStatementFrame: "現在の文の枠線を表示",
|
||||
showCurrentStatementFrameDescription: "有効にすると、SQLエディタで現在実行可能な文を枠線で示します。無効にすると枠線を表示しません。",
|
||||
showInsertValueHints: "INSERT 値の列名ヒントを表示",
|
||||
showInsertValueHintsDescription: "有効にすると、INSERT ... VALUES 句の各値の横に対応する列名ヒントを表示します。",
|
||||
showInsertValueHintsDescription: "有効にすると、INSERT ... VALUES または INSERT ... SELECT の各値/投影の横に対応する列名ヒントを表示します。",
|
||||
previewStatementRunButton: "文の実行ボタンのプレビュー",
|
||||
wordWrap: "折り返し",
|
||||
wordWrapDescription: "長い行をエディタ幅内で折り返します",
|
||||
|
||||
@@ -161,6 +161,8 @@ export default withEnglishFallback({
|
||||
keywordCaseUpper: "SQL 키워드 대문자 사용",
|
||||
sqlSemanticDiagnosticsToggleOn: "SQL 의미 진단: 켜짐 (클릭하여 끄기)",
|
||||
sqlSemanticDiagnosticsToggleOff: "SQL 의미 진단: 꺼짐 (클릭하여 켜기)",
|
||||
insertValueHintsToggleOn: "INSERT 열 이름 힌트: 켜짐 (클릭하여 끄기)",
|
||||
insertValueHintsToggleOff: "INSERT 열 이름 힌트: 꺼짐 (클릭하여 켜기)",
|
||||
autoCommit: "자동 커밋",
|
||||
manualTransaction: "수동 트랜잭션",
|
||||
autoCommitAgent: "자동 커밋 (Agent 백엔드에 따라)",
|
||||
@@ -6075,7 +6077,7 @@ export default withEnglishFallback({
|
||||
showCurrentStatementFrame: "현재 구문 프레임 표시",
|
||||
showCurrentStatementFrameDescription: "활성화하면 SQL 편집기가 현재 실행 가능한 구문 주위에 윤곽선을 그립니다. 비활성화하면 윤곽선이 숨겨집니다.",
|
||||
showInsertValueHints: "INSERT 값 컬럼 힌트 표시",
|
||||
showInsertValueHintsDescription: "활성화하면 SQL 편집기가 INSERT ... VALUES 절의 각 값 옆에 컬럼 이름 힌트를 표시합니다.",
|
||||
showInsertValueHintsDescription: "활성화하면 SQL 편집기가 INSERT ... VALUES 또는 INSERT ... SELECT 절의 각 값/프로젝션 옆에 컬럼 이름 힌트를 표시합니다.",
|
||||
previewStatementRunButton: "구문 실행 버튼 미리보기",
|
||||
wordWrap: "자동 줄바꿈",
|
||||
wordWrapDescription: "편집기 너비 내에서 긴 줄을 줄바꿈합니다",
|
||||
|
||||
@@ -162,6 +162,8 @@ export default withEnglishFallback({
|
||||
keywordCaseUpper: "Usar palavras-chave SQL em maiúsculas",
|
||||
sqlSemanticDiagnosticsToggleOn: "Diagnóstico semântico de SQL: ativado (clique para desativar)",
|
||||
sqlSemanticDiagnosticsToggleOff: "Diagnóstico semântico de SQL: desativado (clique para ativar)",
|
||||
insertValueHintsToggleOn: "Dicas de colunas INSERT: ativadas (clique para desativar)",
|
||||
insertValueHintsToggleOff: "Dicas de colunas INSERT: desativadas (clique para ativar)",
|
||||
autoCommit: "Auto Commit",
|
||||
manualTransaction: "Transação Manual",
|
||||
autoCommitAgent: "Auto Commit (depende do backend do Agente)",
|
||||
@@ -6335,7 +6337,7 @@ export default withEnglishFallback({
|
||||
showCurrentStatementFrame: "Mostrar moldura da instrução atual",
|
||||
showCurrentStatementFrameDescription: "Quando ativado, o editor SQL desenha um contorno ao redor da instrução executável atual; quando desativado, o contorno fica oculto.",
|
||||
showInsertValueHints: "Mostrar dicas de colunas em VALUES de INSERT",
|
||||
showInsertValueHintsDescription: "Quando ativado, o editor SQL mostra o nome da coluna ao lado de cada valor em cláusulas INSERT ... VALUES.",
|
||||
showInsertValueHintsDescription: "Quando ativado, o editor SQL mostra o nome da coluna ao lado de cada valor ou projeção SELECT em cláusulas INSERT ... VALUES e INSERT ... SELECT.",
|
||||
previewStatementRunButton: "Botão de execução de instrução na prévia",
|
||||
wordWrap: "Quebra de linha",
|
||||
wordWrapDescription: "Quebrar linhas longas dentro da largura do editor",
|
||||
|
||||
@@ -85,6 +85,8 @@ export default withEnglishFallback({
|
||||
keywordCaseUpper: "使用大写 SQL 关键字",
|
||||
sqlSemanticDiagnosticsToggleOn: "SQL 语义诊断:已开启(点击关闭)",
|
||||
sqlSemanticDiagnosticsToggleOff: "SQL 语义诊断:已关闭(点击开启)",
|
||||
insertValueHintsToggleOn: "INSERT 列名提示:已开启(点击关闭)",
|
||||
insertValueHintsToggleOff: "INSERT 列名提示:已关闭(点击开启)",
|
||||
autoCommit: "自动提交",
|
||||
manualTransaction: "手动事务",
|
||||
autoCommitAgent: "自动提交(取决于 Agent 后端数据库)",
|
||||
@@ -6672,7 +6674,7 @@ export default withEnglishFallback({
|
||||
showCurrentStatementFrame: "显示当前语句外框线",
|
||||
showCurrentStatementFrameDescription: "开启后,在 SQL 编辑器中用外框线标出当前可执行语句;关闭后不显示外框线。",
|
||||
showInsertValueHints: "显示 INSERT 值列名提示",
|
||||
showInsertValueHintsDescription: "开启后,在 INSERT ... VALUES 子句的每个值旁显示对应列名提示。",
|
||||
showInsertValueHintsDescription: "开启后,在 INSERT ... VALUES 或 INSERT ... SELECT 的每个值/投影旁显示对应列名提示。",
|
||||
previewStatementRunButton: "预览语句执行按钮",
|
||||
wordWrap: "自动换行",
|
||||
wordWrapDescription: "长内容在编辑器宽度内自动折行显示",
|
||||
|
||||
@@ -162,6 +162,8 @@ export default withEnglishFallback({
|
||||
keywordCaseUpper: "使用大寫 SQL 關鍵字",
|
||||
sqlSemanticDiagnosticsToggleOn: "SQL 語意診斷:已開啟(點擊關閉)",
|
||||
sqlSemanticDiagnosticsToggleOff: "SQL 語意診斷:已關閉(點擊開啟)",
|
||||
insertValueHintsToggleOn: "INSERT 欄位提示:已開啟(點擊關閉)",
|
||||
insertValueHintsToggleOff: "INSERT 欄位提示:已關閉(點擊開啟)",
|
||||
autoCommit: "自動提交",
|
||||
manualTransaction: "手動事務",
|
||||
autoCommitAgent: "自動提交(取決於 Agent 後端資料庫)",
|
||||
@@ -5649,7 +5651,7 @@ export default withEnglishFallback({
|
||||
showCurrentStatementFrame: "顯示目前語句外框線",
|
||||
showCurrentStatementFrameDescription: "啟用後,在 SQL 編輯器中用外框線標出目前可執行語句;關閉後不顯示外框線。",
|
||||
showInsertValueHints: "顯示 INSERT 值欄位提示",
|
||||
showInsertValueHintsDescription: "啟用後,在 INSERT ... VALUES 子句的每個值旁顯示對應欄位名稱提示。",
|
||||
showInsertValueHintsDescription: "啟用後,在 INSERT ... VALUES 或 INSERT ... SELECT 的每個值/投影旁顯示對應欄位名稱提示。",
|
||||
previewStatementRunButton: "預覽語句執行按鈕",
|
||||
wordWrap: "自動換行",
|
||||
wordWrapDescription: "長內容在編輯器寬度內自動折行顯示",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { strict as assert } from "node:assert";
|
||||
import { test } from "vitest";
|
||||
import { buildInsertValueHintDecorations, insertValueHintsRefreshDispatchSpec, isInsertValueHintDecorationAt } from "@/lib/editor/codemirrorInsertValueHints";
|
||||
|
||||
test("insertValueHintsRefreshDispatchSpec disables scrollIntoView", () => {
|
||||
assert.equal(insertValueHintsRefreshDispatchSpec.scrollIntoView, false);
|
||||
});
|
||||
|
||||
test("isInsertValueHintDecorationAt detects widget anchors", () => {
|
||||
const sql = "INSERT INTO t (a, b) VALUES (1, 2)";
|
||||
const valueOne = sql.indexOf("1");
|
||||
const valueTwo = sql.indexOf("2");
|
||||
const decorations = buildInsertValueHintDecorations([
|
||||
{ from: valueOne, column: "a" },
|
||||
{ from: valueTwo, column: "b" },
|
||||
]);
|
||||
assert.equal(isInsertValueHintDecorationAt(decorations, valueOne), true);
|
||||
assert.equal(isInsertValueHintDecorationAt(decorations, valueTwo), true);
|
||||
assert.equal(isInsertValueHintDecorationAt(decorations, sql.indexOf("INSERT")), false);
|
||||
});
|
||||
@@ -1,9 +1,15 @@
|
||||
import { StateEffect, type Extension } from "@codemirror/state";
|
||||
import { EditorSelection, StateEffect, type Extension } from "@codemirror/state";
|
||||
import { Decoration, EditorView, ViewPlugin, WidgetType, type DecorationSet, type ViewUpdate } from "@codemirror/view";
|
||||
import { buildInsertValueHints, expandToSqlStatementWindow, insertValueHintsNeedTableColumns, parseInsertValuesClauses, type InsertValueHint, type InsertValuesClause, type TextRange } from "@/lib/sql/insertValueHints";
|
||||
import { matchDollarQuoteTag } from "@/lib/sql/semantic/tokens";
|
||||
import { buildInsertValueHints, expandToSqlStatementWindow, findEnclosingDollarQuoteStartInText, insertValueHintsNeedTableColumns, parseInsertValuesClauses, type InsertValueHint, type InsertValuesClause, type TextRange } from "@/lib/sql/insertValueHints";
|
||||
|
||||
export const refreshInsertValueHintsEffect = StateEffect.define<null>();
|
||||
|
||||
/** Shared dispatch options so hint refreshes never scroll the viewport. */
|
||||
export const insertValueHintsRefreshDispatchSpec = {
|
||||
scrollIntoView: false as const,
|
||||
};
|
||||
|
||||
/** Debounce hint reparse while the user is typing / holding a key. */
|
||||
const INSERT_HINT_REPARSE_DELAY_MS = 80;
|
||||
|
||||
@@ -57,6 +63,30 @@ function decorationsForHints(hints: readonly InsertValueHint[]): DecorationSet {
|
||||
return Decoration.set(ranges);
|
||||
}
|
||||
|
||||
/** @internal Exported for unit tests. */
|
||||
/**
|
||||
* Whether a database type runs SQL-family statements, where INSERT column
|
||||
* hints (and their SELECT extension) apply. NoSQL/document stores are excluded;
|
||||
* keep this list in sync instead of re-inlining it at call sites.
|
||||
*/
|
||||
export function supportsInsertValueHints(databaseType: string | undefined | null): boolean {
|
||||
return databaseType !== "redis" && databaseType !== "mongodb" && databaseType !== "elasticsearch" && databaseType !== "easysearch" && databaseType !== "meilisearch" && databaseType !== "victoriametrics";
|
||||
}
|
||||
|
||||
export function buildInsertValueHintDecorations(hints: readonly InsertValueHint[]): DecorationSet {
|
||||
return decorationsForHints(hints);
|
||||
}
|
||||
|
||||
/** True when `pos` sits on an insert-value-hint widget anchor (value expression start). */
|
||||
export function isInsertValueHintDecorationAt(decorations: DecorationSet, pos: number): boolean {
|
||||
let found = false;
|
||||
decorations.between(Math.max(0, pos - 1), pos + 1, (_from, _to, decoration) => {
|
||||
const widget = decoration.spec?.widget;
|
||||
if (widget instanceof InsertValueHintWidget) found = true;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
function interestRanges(view: EditorView): TextRange[] {
|
||||
const ranges: TextRange[] = view.visibleRanges.map((range) => ({ from: range.from, to: range.to }));
|
||||
const cursor = view.state.selection.main.head;
|
||||
@@ -82,8 +112,20 @@ function parseClausesNearView(view: EditorView, dialectId: string): InsertValues
|
||||
for (const range of interestRanges(view)) {
|
||||
// Pull a bounded neighborhood from the doc instead of materializing the whole script.
|
||||
const pad = 32 * 1024;
|
||||
const sliceFrom = Math.max(0, range.from - pad);
|
||||
let sliceFrom = Math.max(0, range.from - pad);
|
||||
// When the cursor sits inside a PostgreSQL dollar-quoted routine body, a slice that still
|
||||
// includes the opening $tag$ makes expandToSqlStatementWindow treat the whole body as one
|
||||
// opaque literal. Advance sliceFrom past the opening delimiter instead.
|
||||
const sliceTo = Math.min(doc.length, range.to + pad);
|
||||
let dollarQuoteStart: number | null = null;
|
||||
if (sliceTo > sliceFrom && doc.sliceString(sliceFrom, sliceTo).includes("$")) {
|
||||
const probePos = Math.min(doc.length, Math.max(range.from, range.to));
|
||||
dollarQuoteStart = findEnclosingDollarQuoteStartInText(doc, probePos) ?? findEnclosingDollarQuoteStartInText(doc, range.from);
|
||||
}
|
||||
if (dollarQuoteStart !== null) {
|
||||
const openingTag = matchDollarQuoteTag(doc.sliceString(dollarQuoteStart, Math.min(doc.length, dollarQuoteStart + 64)), 0);
|
||||
if (openingTag) sliceFrom = Math.max(sliceFrom, dollarQuoteStart + openingTag.length);
|
||||
}
|
||||
if (sliceTo <= sliceFrom) continue;
|
||||
const slice = doc.sliceString(sliceFrom, sliceTo);
|
||||
const window = expandToSqlStatementWindow(slice, range.from - sliceFrom, range.to - sliceFrom, dialectId);
|
||||
@@ -185,7 +227,10 @@ export function createInsertValueHintsExtension(options: InsertValueHintsExtensi
|
||||
this.reparseTimer = setTimeout(() => {
|
||||
this.reparseTimer = null;
|
||||
// Trigger a lightweight transaction so update() runs compute once typing pauses.
|
||||
view.dispatch({ effects: refreshInsertValueHintsEffect.of(null) });
|
||||
view.dispatch({
|
||||
effects: refreshInsertValueHintsEffect.of(null),
|
||||
...insertValueHintsRefreshDispatchSpec,
|
||||
});
|
||||
}, INSERT_HINT_REPARSE_DELAY_MS);
|
||||
}
|
||||
|
||||
@@ -204,11 +249,31 @@ export function createInsertValueHintsExtension(options: InsertValueHintsExtensi
|
||||
{ decorations: (value) => value.decorations },
|
||||
);
|
||||
|
||||
return [insertValueHintsTheme, plugin];
|
||||
const clickWithoutScroll = EditorView.domEventHandlers({
|
||||
mousedown(event, view) {
|
||||
if (!(options.isEnabled?.() ?? true)) return false;
|
||||
const pluginState = view.plugin(plugin);
|
||||
if (!pluginState) return false;
|
||||
const pos = view.posAtCoords({ x: event.clientX, y: event.clientY });
|
||||
if (pos == null || !isInsertValueHintDecorationAt(pluginState.decorations, pos)) return false;
|
||||
event.preventDefault();
|
||||
view.dispatch({
|
||||
selection: EditorSelection.cursor(pos),
|
||||
...insertValueHintsRefreshDispatchSpec,
|
||||
});
|
||||
view.focus();
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
return [insertValueHintsTheme, plugin, clickWithoutScroll];
|
||||
}
|
||||
|
||||
export function requestInsertValueHintsRefresh(view: EditorView) {
|
||||
view.dispatch({ effects: refreshInsertValueHintsEffect.of(null) });
|
||||
view.dispatch({
|
||||
effects: refreshInsertValueHintsEffect.of(null),
|
||||
...insertValueHintsRefreshDispatchSpec,
|
||||
});
|
||||
}
|
||||
|
||||
export { insertValueHintsNeedTableColumns };
|
||||
@@ -173,6 +173,14 @@ function findWordIndexAtDepth(tokens: readonly SqlSemanticToken[], word: string,
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** INSERT ... (SELECT ...) wraps the source query in an extra paren pair at sourceDepth. */
|
||||
function findInsertSourceSelectIndex(tokens: readonly SqlSemanticToken[], from: number, sourceDepth: number): number {
|
||||
if (tokens[from]?.text === "(" && tokens[from + 1]?.depth === sourceDepth + 1 && tokens[from + 1]?.kind === "word" && tokens[from + 1]?.normalized === "select") {
|
||||
return from + 1;
|
||||
}
|
||||
return findWordIndexAtDepth(tokens, "select", from, sourceDepth);
|
||||
}
|
||||
|
||||
function findClosingParenIndex(tokens: readonly SqlSemanticToken[], openIndex: number): number {
|
||||
const open = tokens[openIndex];
|
||||
if (!open || open.text !== "(") return -1;
|
||||
@@ -203,8 +211,16 @@ function selectProjectionStarts(tokens: readonly SqlSemanticToken[], selectIndex
|
||||
const depth = select.depth;
|
||||
let index = selectIndex + 1;
|
||||
|
||||
if (tokens[index]?.depth === depth && tokens[index]?.kind === "word" && (tokens[index]?.normalized === "all" || tokens[index]?.normalized === "distinct")) {
|
||||
if (tokens[index]?.depth === depth && tokens[index]?.kind === "word" && tokens[index]?.normalized === "all") {
|
||||
index += 1;
|
||||
} else if (tokens[index]?.depth === depth && tokens[index]?.kind === "word" && tokens[index]?.normalized === "distinct") {
|
||||
index += 1;
|
||||
// PostgreSQL DISTINCT ON (cols) — skip the ON (...) clause before splitting projections.
|
||||
if (tokens[index]?.depth === depth && tokens[index]?.kind === "word" && tokens[index]?.normalized === "on" && tokens[index + 1]?.text === "(") {
|
||||
const closeIndex = findClosingParenIndex(tokens, index + 1);
|
||||
if (closeIndex < 0) return [];
|
||||
index = closeIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const maybeTop = tokens[index];
|
||||
@@ -278,7 +294,7 @@ function parseInsertClause(tokens: readonly SqlSemanticToken[], span: SqlSemanti
|
||||
|
||||
const sourceDepth = tokens[insertIndex]?.depth ?? 0;
|
||||
const valuesIndex = findWordIndexAtDepth(tokens, "values", index, sourceDepth);
|
||||
const selectIndex = findWordIndexAtDepth(tokens, "select", index, sourceDepth);
|
||||
const selectIndex = findInsertSourceSelectIndex(tokens, index, sourceDepth);
|
||||
if (valuesIndex < 0 && selectIndex < 0) return null;
|
||||
|
||||
const rows = selectIndex >= 0 && (valuesIndex < 0 || selectIndex < valuesIndex) ? [selectProjectionStarts(tokens, selectIndex)] : parseValuesRows(tokens, valuesIndex);
|
||||
@@ -540,6 +556,60 @@ interface StatementWindowMemo {
|
||||
// function -- a cache hit is guaranteed to be the same answer, not a staleness risk.
|
||||
let statementWindowMemo: StatementWindowMemo | null = null;
|
||||
|
||||
export interface SqlTextSlice {
|
||||
readonly length: number;
|
||||
sliceString(from: number, to: number): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* When `position` sits inside an unclosed PostgreSQL dollar-quoted body (`$tag$ ...`), return the
|
||||
* opening tag offset; otherwise `null`. Scans from document start so callers can advance a bounded
|
||||
* editor slice to start just past the opening delimiter (not include it) before statement-window
|
||||
* heuristics run.
|
||||
*/
|
||||
export function findEnclosingDollarQuoteStart(sql: string, position: number): number | null {
|
||||
return findEnclosingDollarQuoteStartInText({ length: sql.length, sliceString: (from, to) => sql.slice(from, to) }, position);
|
||||
}
|
||||
|
||||
const DOLLAR_QUOTE_SLICE_SCAN_CHUNK = 64 * 1024;
|
||||
/** When a chunk ends on `$`, extend the read so a `$tag$` marker split across chunks is still visible. */
|
||||
const DOLLAR_QUOTE_CHUNK_TAIL_EXTENSION = 128;
|
||||
|
||||
export function findEnclosingDollarQuoteStartInText(doc: SqlTextSlice, position: number): number | null {
|
||||
let openTag: string | null = null;
|
||||
let openPos = -1;
|
||||
const end = Math.max(0, Math.min(position, doc.length));
|
||||
let scanned = 0;
|
||||
while (scanned < end) {
|
||||
let chunkEnd = Math.min(scanned + DOLLAR_QUOTE_SLICE_SCAN_CHUNK, end);
|
||||
if (chunkEnd < end && chunkEnd > scanned && doc.sliceString(chunkEnd - 1, chunkEnd) === "$") {
|
||||
chunkEnd = Math.min(chunkEnd + DOLLAR_QUOTE_CHUNK_TAIL_EXTENSION, end);
|
||||
}
|
||||
const chunk = doc.sliceString(scanned, chunkEnd);
|
||||
for (let local = 0; local < chunk.length; ) {
|
||||
const index = scanned + local;
|
||||
if (index >= end) break;
|
||||
if (chunk[local] === "$") {
|
||||
const marker = matchDollarQuoteTag(chunk, local);
|
||||
if (marker) {
|
||||
if (openTag === null) {
|
||||
openTag = marker;
|
||||
openPos = index;
|
||||
} else if (marker === openTag) {
|
||||
openTag = null;
|
||||
openPos = -1;
|
||||
}
|
||||
local += marker.length;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
local += 1;
|
||||
}
|
||||
scanned = chunkEnd;
|
||||
}
|
||||
return openTag !== null ? openPos : null;
|
||||
}
|
||||
|
||||
export function expandToSqlStatementWindow(sql: string, from: number, to: number, dialectId = "mysql"): TextRange {
|
||||
if (statementWindowMemo && statementWindowMemo.sql === sql && statementWindowMemo.from === from && statementWindowMemo.to === to && statementWindowMemo.dialectId === dialectId) {
|
||||
return statementWindowMemo.result;
|
||||
|
||||
Reference in new issue
Block a user