fix(editor): split query editor into focused components
This commit is contained in:
63 files changed
+11857
-7011
No files matched your search
+2
-2
@@ -9,10 +9,10 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
// completion row other than the checkbox itself used to fall through to
|
||||
// CodeMirror's own list-item click handler, which applied that single row
|
||||
// as the completion and silently discarded every other checked field. See
|
||||
// onBatchColumnSelectionRowGuard in QueryEditor.vue.
|
||||
// onBatchColumnSelectionRowGuard in useQueryEditorBatchSelection.ts.
|
||||
// happy-dom's global URL polyfill rejects the file: scheme, so resolve the
|
||||
// path with Node's own path/url helpers instead of `new URL(..., import.meta.url)`.
|
||||
const queryEditorSource = readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "../../../components/editor/QueryEditor.vue"), "utf8");
|
||||
const queryEditorSource = readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "../../../components/editor/useQueryEditorBatchSelection.ts"), "utf8");
|
||||
|
||||
function extractFunction(name: string): string {
|
||||
const start = queryEditorSource.indexOf(`function ${name}(`);
|
||||
|
||||
+2
-2
@@ -7,10 +7,10 @@ import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
// Regression coverage for #9122: the batch-selection completion list offered a
|
||||
// per-field checkbox plus an "insert selected fields" row, but no way to check
|
||||
// every field at once. See toggleAllBatchColumnSelection in QueryEditor.vue.
|
||||
// every field at once. See toggleAllBatchColumnSelection in useQueryEditorBatchSelection.ts.
|
||||
// happy-dom's global URL polyfill rejects the file: scheme, so resolve the
|
||||
// path with Node's own path/url helpers instead of `new URL(..., import.meta.url)`.
|
||||
const queryEditorSource = readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "../../../components/editor/QueryEditor.vue"), "utf8");
|
||||
const queryEditorSource = readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "../../../components/editor/useQueryEditorBatchSelection.ts"), "utf8");
|
||||
|
||||
function extractFunction(name: string): string {
|
||||
const start = queryEditorSource.indexOf(`function ${name}(`);
|
||||
|
||||
@@ -6,6 +6,10 @@ import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
|
||||
|
||||
const contextMenuSource = readFileSync(new URL("../../../components/editor/QueryEditorContextMenu.vue", import.meta.url), "utf8");
|
||||
|
||||
const runtimeSource = readFileSync(new URL("../../../components/editor/queryEditorCodeMirrorRuntime.ts", import.meta.url), "utf8");
|
||||
|
||||
function runToggleBlockComment(doc: string, selection: EditorSelection | { anchor: number; head?: number }) {
|
||||
let state = EditorState.create({ doc, selection, extensions: [sql()] });
|
||||
const dispatch = vi.fn((transaction: Transaction) => {
|
||||
@@ -23,15 +27,16 @@ function runToggleBlockComment(doc: string, selection: EditorSelection | { ancho
|
||||
|
||||
describe("QueryEditor block comment", () => {
|
||||
it("wires toggleBlockComment into the right-click context menu, the keymap, and the editor module load", () => {
|
||||
expect(queryEditorSource).toContain('let codeMirrorToggleBlockComment: typeof import("@codemirror/commands").toggleBlockComment | null = null;');
|
||||
expect(runtimeSource).toContain('codeMirrorToggleBlockComment: typeof import("@codemirror/commands").toggleBlockComment | null;');
|
||||
expect(runtimeSource).toContain("codeMirrorToggleBlockComment: null,");
|
||||
expect(queryEditorSource).toContain("codeMirrorToggleBlockComment?.(currentView);");
|
||||
expect(queryEditorSource).toContain('label: t("editor.contextMenu.blockCommentSelection")');
|
||||
expect(queryEditorSource).toContain("shortcut: shortcuts.toggleBlockComment");
|
||||
expect(contextMenuSource).toContain('label: t("editor.contextMenu.blockCommentSelection")');
|
||||
expect(contextMenuSource).toContain("shortcut: shortcuts.toggleBlockComment");
|
||||
expect(queryEditorSource).toContain("...binding(shortcuts.toggleBlockComment, (view) => {");
|
||||
expect(queryEditorSource).toContain("!supportsQueryEditorBlockComments(props.databaseType)");
|
||||
expect(queryEditorSource).toContain("defaultKeymapForGlobalShortcuts(codeMirrorDefaultKeymap, settingsStore.editorSettings.shortcuts).filter((item) => item.run !== codeMirrorToggleBlockComment)");
|
||||
expect(queryEditorSource).toContain("defaultKeymapForGlobalShortcuts(codeMirrorRuntime.codeMirrorDefaultKeymap, settingsStore.editorSettings.shortcuts).filter((item) => item.run !== codeMirrorRuntime.codeMirrorToggleBlockComment)");
|
||||
expect(queryEditorSource).toContain("defaultKeymapComp.reconfigure(defaultKeymapExtension())");
|
||||
expect(queryEditorSource).toContain("codeMirrorToggleBlockComment = toggleBlockComment;");
|
||||
expect(runtimeSource).toContain("runtime.codeMirrorToggleBlockComment = toggleBlockComment;");
|
||||
});
|
||||
|
||||
it("wraps the selected SQL in a block comment", () => {
|
||||
|
||||
@@ -7,15 +7,21 @@ import { DEFAULT_SHORTCUT_SETTINGS, normalizeShortcutSettings, shortcutToCodeMir
|
||||
|
||||
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
|
||||
|
||||
const batchSource = readFileSync(new URL("../../../components/editor/useQueryEditorBatchSelection.ts", import.meta.url), "utf8");
|
||||
|
||||
const completionSource = readFileSync(new URL("../../../components/editor/useQueryEditorCompletion.ts", import.meta.url), "utf8");
|
||||
const completionKeysSource = readFileSync(new URL("../../../components/editor/useQueryEditorCompletionKeys.ts", import.meta.url), "utf8");
|
||||
|
||||
function extractFunction(name: string): string {
|
||||
const start = queryEditorSource.indexOf(`function ${name}(`);
|
||||
const source = [queryEditorSource, completionSource, batchSource, completionKeysSource].find((candidate) => candidate.includes(`function ${name}(`)) ?? "";
|
||||
const start = source.indexOf(`function ${name}(`);
|
||||
if (start < 0) throw new Error(`Missing QueryEditor function: ${name}`);
|
||||
const bodyStart = queryEditorSource.indexOf("{", start);
|
||||
const bodyStart = source.indexOf("{", start);
|
||||
let depth = 0;
|
||||
for (let index = bodyStart; index < queryEditorSource.length; index++) {
|
||||
const character = queryEditorSource[index];
|
||||
for (let index = bodyStart; index < source.length; index++) {
|
||||
const character = source[index];
|
||||
if (character === "{") depth++;
|
||||
if (character === "}" && --depth === 0) return queryEditorSource.slice(start, index + 1);
|
||||
if (character === "}" && --depth === 0) return source.slice(start, index + 1);
|
||||
}
|
||||
throw new Error(`Unterminated QueryEditor function: ${name}`);
|
||||
}
|
||||
@@ -84,6 +90,8 @@ function createHarness(options: {
|
||||
"let pendingCompletionTabTimer: ReturnType<typeof setTimeout> | null = null;",
|
||||
"let cancelPendingCompletionEnter: (() => void) | null = null;",
|
||||
"let suppressNextSqlCompletionAutoStartUntil = 0;",
|
||||
"const completion = { get suppressAutoStartUntil() { return suppressNextSqlCompletionAutoStartUntil; }, set suppressAutoStartUntil(value) { suppressNextSqlCompletionAutoStartUntil = value; } };",
|
||||
"const codeMirrorRuntime = { codeMirrorCompletionStatus, codeMirrorSelectedCompletion, codeMirrorAcceptCompletion, codeMirrorSelectedCompletionIndex, codeMirrorSelectFirstCompletion, codeMirrorCloseCompletion, codeMirrorInsertNewlineKeepIndent, codeMirrorNextSnippetField, codeMirrorIndentMore };",
|
||||
extractFunction("editorIndentUnit"),
|
||||
extractFunction("handleTab"),
|
||||
extractFunction("tabKeyAcceptsCompletion"),
|
||||
@@ -181,7 +189,7 @@ afterEach(() => {
|
||||
|
||||
describe("QueryEditor completion Tab keymap", () => {
|
||||
it("guards the batch INSERT snippet factory before applying", () => {
|
||||
expect(queryEditorSource).toContain('if (session.mode === "insert" && !codeMirrorSnippetCompletion)');
|
||||
expect(batchSource).toContain('if (session.mode === "insert" && !runtime.codeMirrorSnippetCompletion)');
|
||||
});
|
||||
it("closes completion and suppresses its restart for the Enter newline", () => {
|
||||
const closeCompletion = vi.fn(() => true);
|
||||
@@ -554,7 +562,7 @@ describe("QueryEditor completion Tab keymap", () => {
|
||||
const javascript = ts.transpileModule(source, {
|
||||
compilerOptions: { module: ts.ModuleKind.None, target: ts.ScriptTarget.ES2022 },
|
||||
}).outputText;
|
||||
const factory = new Function("codeMirrorStartCompletion", `${javascript}\nreturn { triggerSqlCompletion, isEditorComposing };`);
|
||||
const factory = new Function("codeMirrorStartCompletion", `const runtime = { codeMirrorStartCompletion };\n${javascript}\nreturn { triggerSqlCompletion, isEditorComposing };`);
|
||||
const startCompletion = vi.fn(() => true);
|
||||
const { triggerSqlCompletion } = factory(startCompletion) as {
|
||||
triggerSqlCompletion: (view: MockView) => boolean;
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import { EditorView, keymap, runScopeHandlers } from "@codemirror/view";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { acceptSelectedCompletionWithRetry, acceptSelectedOrFirstCompletion } from "@/lib/editor/queryEditorCompletionAcceptance";
|
||||
|
||||
const queryEditorSource = readFileSync(resolve(process.cwd(), "apps/desktop/src/components/editor/QueryEditor.vue"), "utf8");
|
||||
const queryEditorSource = readFileSync(resolve(process.cwd(), "apps/desktop/src/components/editor/queryEditorSqlExtensions.ts"), "utf8");
|
||||
|
||||
function createCompletionView(doc = "", selectFirstCompletionOnOpen = false, interactionDelay: number | null = 0) {
|
||||
return new EditorView({
|
||||
|
||||
+15
-9
@@ -3,15 +3,21 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
|
||||
|
||||
const diagnosticsSource = readFileSync(new URL("../../../components/editor/useQueryEditorDiagnostics.ts", import.meta.url), "utf8");
|
||||
|
||||
const completionSource = readFileSync(new URL("../../../components/editor/useQueryEditorCompletion.ts", import.meta.url), "utf8");
|
||||
const metadataSource = readFileSync(new URL("../../../components/editor/useQueryEditorCompletionMetadata.ts", import.meta.url), "utf8");
|
||||
|
||||
function extractFunction(name: string): string {
|
||||
const start = queryEditorSource.indexOf(`function ${name}(`);
|
||||
const source = [queryEditorSource, completionSource, metadataSource, diagnosticsSource].find((candidate) => candidate.includes(`function ${name}(`)) ?? "";
|
||||
const start = source.indexOf(`function ${name}(`);
|
||||
if (start < 0) throw new Error(`Missing QueryEditor function: ${name}`);
|
||||
const bodyStart = queryEditorSource.indexOf("{", start);
|
||||
const bodyStart = source.indexOf("{", start);
|
||||
let depth = 0;
|
||||
for (let index = bodyStart; index < queryEditorSource.length; index++) {
|
||||
const character = queryEditorSource[index];
|
||||
for (let index = bodyStart; index < source.length; index++) {
|
||||
const character = source[index];
|
||||
if (character === "{") depth++;
|
||||
if (character === "}" && --depth === 0) return queryEditorSource.slice(start, index + 1);
|
||||
if (character === "}" && --depth === 0) return source.slice(start, index + 1);
|
||||
}
|
||||
throw new Error(`Unterminated QueryEditor function: ${name}`);
|
||||
}
|
||||
@@ -43,7 +49,7 @@ describe("QueryEditor database name completion wiring", () => {
|
||||
expect(extractFunction("routineCompletionScopeForContext")).toContain("database: target.database");
|
||||
expect(extractFunction("completionObjectScopeKey")).toContain("scope.database");
|
||||
expect(extractFunction("completionObjectScopeKey")).toContain("scope.schema");
|
||||
expect(queryEditorSource).toContain("cachedCompletionObjectsByScope");
|
||||
expect(metadataSource).toContain("cachedCompletionObjectsByScope");
|
||||
});
|
||||
|
||||
it("loads SQL Server capability metadata before offering or applying USE completion", () => {
|
||||
@@ -58,8 +64,8 @@ describe("QueryEditor database name completion wiring", () => {
|
||||
const scope = extractFunction("semanticDiagnosticMetadataScope");
|
||||
expect(scope).toContain("sqlServerUseDatabaseBeforeCursor(sql, range.from)");
|
||||
expect(scope).toContain("lookupLocalCompletionDatabases");
|
||||
expect(queryEditorSource).toMatch(/async function refreshSemanticDiagnostics[\s\S]*?semanticDiagnosticTablesForScope\(semanticAnalysis\.tables, metadataScope\)/);
|
||||
expect(queryEditorSource).toContain("enrichSemanticDiagnosticTables(scopedAnalysis.tables, metadataScope)");
|
||||
expect(queryEditorSource).toContain("ensureColumnsForSemanticDiagnostics(tables, metadataScope)");
|
||||
expect(diagnosticsSource).toMatch(/async function refreshSemanticDiagnostics[\s\S]*?semanticDiagnosticTablesForScope\(semanticAnalysis\.tables, metadataScope\)/);
|
||||
expect(diagnosticsSource).toContain("enrichSemanticDiagnosticTables(scopedAnalysis.tables, metadataScope)");
|
||||
expect(diagnosticsSource).toContain("ensureColumnsForSemanticDiagnostics(tables, metadataScope)");
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -49,7 +49,7 @@ describe("needsDiagnosticCaretReanchor", () => {
|
||||
});
|
||||
|
||||
describe("QueryEditor diagnostic caret re-anchor wiring", () => {
|
||||
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
|
||||
const queryEditorSource = readFileSync(new URL("../../../components/editor/useQueryEditorDiagnostics.ts", import.meta.url), "utf8");
|
||||
|
||||
it("re-anchors the browser caret after applying diagnostics, outside a transaction", () => {
|
||||
expect(queryEditorSource).toMatch(/function reconfigureDiagnostics\(\)[\s\S]*?reanchorCaretAfterDiagnostics\(currentView\)/);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createQueryEditorExecutionViewportOwnership, isQueryEditorPositionVisible } from "../../editor/queryEditorExecutionViewport";
|
||||
|
||||
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
|
||||
const queryEditorSource = ["QueryEditor.vue", "useQueryEditorExecution.ts"].map((file) => readFileSync(new URL(`../../../components/editor/${file}`, import.meta.url), "utf8")).join("\n");
|
||||
const contentAreaSource = readFileSync(new URL("../../../components/layout/ContentArea.vue", import.meta.url), "utf8");
|
||||
const editorToolbarSource = readFileSync(new URL("../../../components/layout/EditorToolbar.vue", import.meta.url), "utf8");
|
||||
const editorGroupSource = readFileSync(new URL("../../../components/layout/EditorGroup.vue", import.meta.url), "utf8");
|
||||
@@ -148,7 +148,7 @@ describe("QueryEditor execution routing", () => {
|
||||
expect(queryEditorSource).toContain("shouldBlockExecutionShortcut(event, currentView)");
|
||||
expect(queryEditorSource).toContain("if (props.readOnly) return true;");
|
||||
expect(queryEditorSource).toContain("settingsStore.editorSettings.sqlShortcuts");
|
||||
expect(queryEditorSource).toContain("runKeymapComp.reconfigure(runKeymapExtension(editorViewModule.keymap))");
|
||||
expect(queryEditorSource).toContain("runKeymapComp.reconfigure(runKeymapExtension(codeMirrorRuntime.editorViewModule.keymap))");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { focusEditorView, type EditorViewLike } from "@/lib/editor/queryEditorFocus";
|
||||
|
||||
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
|
||||
const queryEditorSource = ["QueryEditor.vue", "queryEditorTypes.ts", "useQueryEditorExecution.ts", "useQueryEditorDocumentState.ts"].map((file) => readFileSync(new URL(`../../../components/editor/${file}`, import.meta.url), "utf8")).join("\n");
|
||||
const contentAreaSource = readFileSync(new URL("../../../components/layout/ContentArea.vue", import.meta.url), "utf8");
|
||||
const editorToolbarSource = readFileSync(new URL("../../../components/layout/EditorToolbar.vue", import.meta.url), "utf8");
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
|
||||
const queryEditorSource = readFileSync(new URL("../../../components/editor/useQueryEditorHover.ts", import.meta.url), "utf8");
|
||||
const contentSource = readFileSync(new URL("../../../components/editor/queryEditorHoverContent.ts", import.meta.url), "utf8");
|
||||
|
||||
describe("QueryEditor hover DDL copy", () => {
|
||||
it("reconciles the local sidebar index before remote hover fallback", () => {
|
||||
const hoverStart = queryEditorSource.indexOf("async function resolveSqlHoverTooltip");
|
||||
const hoverEnd = queryEditorSource.indexOf("\n}\n\nfunction sqlErrorDecorationRange", hoverStart);
|
||||
const hoverEnd = queryEditorSource.indexOf("\n }\n", hoverStart);
|
||||
const hoverSource = queryEditorSource.slice(hoverStart, hoverEnd);
|
||||
const localLookup = hoverSource.indexOf("connectionStore.lookupLocalCompletionTables");
|
||||
const remoteLookup = hoverSource.indexOf("connectionStore.listCompletionTables");
|
||||
@@ -16,7 +17,7 @@ describe("QueryEditor hover DDL copy", () => {
|
||||
expect(localLookup).toBeGreaterThanOrEqual(0);
|
||||
expect(remoteLookup).toBeGreaterThan(localLookup);
|
||||
expect(hoverSource).toContain("hoverTables = mergeCompletionTables(localHoverTables, hoverTables);");
|
||||
expect(hoverSource).toContain("cachedTables = mergeCompletionTables(localHoverTables, cachedTables);");
|
||||
expect(hoverSource).toContain("completionMetadata.cachedTables = mergeCompletionTables(localHoverTables, completionMetadata.cachedTables);");
|
||||
});
|
||||
|
||||
it("gates only the table DDL branch on the preference, keeping column hover", () => {
|
||||
@@ -25,12 +26,12 @@ describe("QueryEditor hover DDL copy", () => {
|
||||
});
|
||||
|
||||
it("adds an accessible copy button that preserves SQL whitespace semantics", () => {
|
||||
expect(queryEditorSource).toContain('copyButton.textContent = t("grid.copyDdl");');
|
||||
expect(queryEditorSource).toContain('copyButton.setAttribute("aria-label", t("grid.copyDdl"));');
|
||||
expect(queryEditorSource).toContain('copyButton.addEventListener("pointerdown"');
|
||||
expect(queryEditorSource).toContain("event.stopPropagation();");
|
||||
expect(queryEditorSource).toContain("await copyToClipboard(normalizeAlignedSqlWhitespace(sqlContent));");
|
||||
expect(queryEditorSource).toContain('toast(t("contextMenu.ddlCopied"), 2000);');
|
||||
expect(queryEditorSource).toContain('toast(t("grid.copyFailed", { message: error?.message || String(error) }), 5000);');
|
||||
expect(contentSource).toContain('copyButton.textContent = t("grid.copyDdl");');
|
||||
expect(contentSource).toContain('copyButton.setAttribute("aria-label", t("grid.copyDdl"));');
|
||||
expect(contentSource).toContain('copyButton.addEventListener("pointerdown"');
|
||||
expect(contentSource).toContain("event.stopPropagation();");
|
||||
expect(contentSource).toContain("await copyToClipboard(normalizeAlignedSqlWhitespace(sqlContent));");
|
||||
expect(contentSource).toContain('toast(t("contextMenu.ddlCopied"), 2000);');
|
||||
expect(contentSource).toContain('toast(t("grid.copyFailed", { message: error?.message || String(error) }), 5000);');
|
||||
});
|
||||
});
|
||||
@@ -5,10 +5,11 @@ import { EditorState, Prec, type Transaction } from "@codemirror/state";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { queryEditorCommentTokens, queryEditorLineCommentToken, queryEditorWordLanguageData } from "@/lib/editor/queryEditorLineComment";
|
||||
|
||||
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
|
||||
const editorThemesSource = readFileSync(new URL("../../editor/editorThemes.ts", import.meta.url), "utf8");
|
||||
const shellHighlightSource = readFileSync(new URL("../../editor/codemirrorShellLineCommentHighlight.ts", import.meta.url), "utf8");
|
||||
|
||||
const extensionsSource = readFileSync(new URL("../../../components/editor/queryEditorSqlExtensions.ts", import.meta.url), "utf8");
|
||||
|
||||
function runToggleLineComment(doc: string, commentToken: string) {
|
||||
let state = EditorState.create({
|
||||
doc,
|
||||
@@ -72,13 +73,13 @@ describe("QueryEditor word selection", () => {
|
||||
|
||||
describe("QueryEditor line comment", () => {
|
||||
it("overrides the language comment tokens in the SQL language compartment", () => {
|
||||
expect(queryEditorSource).toContain("Prec.highest(EditorState.languageData.of(() => [{ commentTokens: queryEditorCommentTokens(props.databaseType) }]))");
|
||||
expect(extensionsSource).toContain("Prec.highest(EditorState.languageData.of(() => [{ commentTokens: queryEditorCommentTokens(props.databaseType) }]))");
|
||||
});
|
||||
|
||||
it("highlights // comments with the theme's comment style", () => {
|
||||
expect(queryEditorSource).toContain('queryEditorLineCommentToken(props.databaseType) === "//" ? shellLineCommentHighlightPlugin : []');
|
||||
expect(queryEditorSource).toContain("const shellLineCommentHighlightPlugin = createShellLineCommentHighlight({ ViewPlugin, Decoration, highlightingFor, syntaxTree });");
|
||||
expect(queryEditorSource).toContain("shellLineCommentTheme(EditorView),");
|
||||
expect(extensionsSource).toContain('queryEditorLineCommentToken(props.databaseType) === "//" ? shellLineCommentHighlightPlugin : []');
|
||||
expect(extensionsSource).toContain("const shellLineCommentHighlightPlugin = createShellLineCommentHighlight({ ViewPlugin, Decoration, highlightingFor, syntaxTree });");
|
||||
expect(extensionsSource).toContain("shellLineCommentTheme(EditorView),");
|
||||
expect(editorThemesSource).toContain('".cm-shell-line-comment *"');
|
||||
expect(editorThemesSource).toContain('color: "inherit !important"');
|
||||
});
|
||||
|
||||
+22
-16
@@ -4,6 +4,10 @@ import { expandToSqlStatementWindow } from "@/lib/sql/insertValueHints";
|
||||
|
||||
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
|
||||
|
||||
const diagnosticsSource = readFileSync(new URL("../../../components/editor/useQueryEditorDiagnostics.ts", import.meta.url), "utf8");
|
||||
|
||||
const extensionsSource = readFileSync(new URL("../../../components/editor/queryEditorSqlExtensions.ts", import.meta.url), "utf8");
|
||||
|
||||
describe("QueryEditor semantic highlighting while scrolling", () => {
|
||||
it("recognizes the large single-statement shape from the reported regression", () => {
|
||||
const sql = `CREATE TABLE [dbo].[code] ([id] int, [label] nvarchar(32));\nINSERT INTO [dbo].[code] ([id], [label]) VALUES\n${Array.from({ length: 240 }, (_, index) => `(${index}, N'row-${String(index).padStart(4, "0")}'),`).join("\n")}`;
|
||||
@@ -15,38 +19,40 @@ describe("QueryEditor semantic highlighting while scrolling", () => {
|
||||
});
|
||||
|
||||
it("reuses semantic statement windows across viewport-only updates", () => {
|
||||
expect(queryEditorSource).toContain('private cachedDoc: import("@codemirror/state").Text | null = null;');
|
||||
expect(queryEditorSource).toContain("private cachedWindows: Array<{");
|
||||
expect(queryEditorSource).toContain("const cached = this.cachedWindows.find");
|
||||
expect(queryEditorSource).toContain("const pendingWindows: Array<{ from: number; to: number }>");
|
||||
expect(queryEditorSource).toContain("MAX_SQL_SEMANTIC_HIGHLIGHT_WINDOWS = 32");
|
||||
expect(queryEditorSource).toContain("this.cachedWindows.splice(0, this.cachedWindows.length - MAX_SQL_SEMANTIC_HIGHLIGHT_WINDOWS)");
|
||||
expect(extensionsSource).toContain('private cachedDoc: import("@codemirror/state").Text | null = null;');
|
||||
expect(extensionsSource).toContain("private cachedWindows: Array<{");
|
||||
expect(extensionsSource).toContain("const cached = this.cachedWindows.find");
|
||||
expect(extensionsSource).toContain("const pendingWindows: Array<{ from: number; to: number }>");
|
||||
expect(extensionsSource).toContain("MAX_SQL_SEMANTIC_HIGHLIGHT_WINDOWS = 32");
|
||||
expect(extensionsSource).toContain("this.cachedWindows.splice(0, this.cachedWindows.length - MAX_SQL_SEMANTIC_HIGHLIGHT_WINDOWS)");
|
||||
});
|
||||
|
||||
it("defers semantic highlighting while the document is changing", () => {
|
||||
expect(queryEditorSource).toContain("SQL_SEMANTIC_HIGHLIGHT_DEBOUNCE_MS = 100");
|
||||
expect(queryEditorSource).toContain("this.decorations = this.decorations.map(update.changes)");
|
||||
expect(queryEditorSource).toContain("refreshSqlSemanticHighlightEffect.of(null)");
|
||||
expect(extensionsSource).toContain("SQL_SEMANTIC_HIGHLIGHT_DEBOUNCE_MS = 100");
|
||||
expect(extensionsSource).toContain("this.decorations = this.decorations.map(update.changes)");
|
||||
expect(extensionsSource).toContain("refreshSqlSemanticHighlightEffect.of(null)");
|
||||
});
|
||||
|
||||
it("never exposes undefined decorations and prewarms the full document only once per document", () => {
|
||||
expect(queryEditorSource).toContain("this.decorations = Decoration.none;");
|
||||
expect(queryEditorSource).toContain('private prewarmedDoc: import("@codemirror/state").Text | null = null;');
|
||||
expect(queryEditorSource).toContain("const shouldPrewarmFullDocument =");
|
||||
expect(queryEditorSource).toContain("if (shouldPrewarmFullDocument) this.prewarmedDoc = doc;");
|
||||
expect(extensionsSource).toContain("this.decorations = Decoration.none;");
|
||||
expect(extensionsSource).toContain('private prewarmedDoc: import("@codemirror/state").Text | null = null;');
|
||||
expect(extensionsSource).toContain("const shouldPrewarmFullDocument =");
|
||||
expect(extensionsSource).toContain("if (shouldPrewarmFullDocument) this.prewarmedDoc = doc;");
|
||||
});
|
||||
|
||||
it("keeps existing highlights while the parser catches up instead of wiping them", () => {
|
||||
// An incomplete Lezer parse must not clear table-name decorations: the
|
||||
// deferred refresh retries, so a freshly mounted editor (tab switch) also
|
||||
// recovers even without a later viewport change.
|
||||
expect(queryEditorSource).toContain("this.scheduleRefresh(currentView);");
|
||||
expect(queryEditorSource).toContain("return this.decorations;");
|
||||
expect(extensionsSource).toContain("this.scheduleRefresh(currentView);");
|
||||
expect(extensionsSource).toContain("return this.decorations;");
|
||||
});
|
||||
|
||||
it("keeps preview and diagnostics on the shared statement-range cache", () => {
|
||||
expect(queryEditorSource).toContain("executableStatementRangeCache = executableStatementRangeCacheForDoc");
|
||||
expect(queryEditorSource).toContain('props.databaseType === "sqlserver" ? undefined : executableStatementRangeCache?.ranges');
|
||||
expect(diagnosticsSource).toContain('props.databaseType === "sqlserver" ? undefined : runtime.executableStatementRangeCache?.ranges');
|
||||
expect(queryEditorSource).toContain("get executableStatementRangeCache()");
|
||||
expect(queryEditorSource).toContain("set executableStatementRangeCache(value)");
|
||||
expect(queryEditorSource).toContain("const cursorRange = executableStatementRangeAtCursor(executableStatementRangeCache, cursorPos)");
|
||||
});
|
||||
});
|
||||
@@ -3,10 +3,13 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
|
||||
|
||||
const runtimeSource = readFileSync(new URL("../../../components/editor/queryEditorCodeMirrorRuntime.ts", import.meta.url), "utf8");
|
||||
|
||||
describe("QueryEditor SQL signature refresh wiring", () => {
|
||||
it("reconfigures signature help when the database dialect or driver profile changes", () => {
|
||||
expect(queryEditorSource).toContain('let sqlSignatureComp: import("@codemirror/state").Compartment | null = null;');
|
||||
expect(queryEditorSource).toContain("sqlSignatureComp.of(buildSqlSignatureExtension())");
|
||||
expect(runtimeSource).toContain('sqlSignatureComp: import("@codemirror/state").Compartment | null;');
|
||||
expect(runtimeSource).toContain("runtime.sqlSignatureComp = new Compartment();");
|
||||
expect(queryEditorSource).toContain("initializedRuntime.sqlSignatureComp.of(sqlExtensions.buildSqlSignatureExtension())");
|
||||
|
||||
const watcherStart = queryEditorSource.indexOf("watch([() => props.databaseType, () => props.dialect, () => props.syntaxDialect, sqlDriverProfile]");
|
||||
const watcherEnd = queryEditorSource.indexOf("\n});", watcherStart);
|
||||
@@ -14,6 +17,6 @@ describe("QueryEditor SQL signature refresh wiring", () => {
|
||||
|
||||
expect(watcherStart).toBeGreaterThanOrEqual(0);
|
||||
expect(watcherEnd).toBeGreaterThan(watcherStart);
|
||||
expect(dialectWatcher).toContain("sqlSignatureComp.reconfigure(buildSqlSignatureExtension())");
|
||||
expect(dialectWatcher).toContain("codeMirrorRuntime.sqlSignatureComp.reconfigure(codeMirrorRuntime.buildSqlSignatureExtension())");
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,9 @@ import { EditorSelection, EditorState } from "@codemirror/state";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { blankLineDeletionChanges, replaceSelectedEditorText } from "@/lib/editor/queryEditorTextEdits";
|
||||
|
||||
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
|
||||
const queryEditorSource = ["QueryEditor.vue", "useQueryEditorTextActions.ts"].map((file) => readFileSync(new URL(`../../../components/editor/${file}`, import.meta.url), "utf8")).join("\n");
|
||||
|
||||
const contextMenuSource = readFileSync(new URL("../../../components/editor/QueryEditorContextMenu.vue", import.meta.url), "utf8");
|
||||
|
||||
function applyBlankLineDeletion(doc: string, from: number, to: number): string {
|
||||
const state = EditorState.create({
|
||||
@@ -27,7 +29,7 @@ describe("replaceSelectedEditorText", () => {
|
||||
});
|
||||
|
||||
it("disables and guards delimited-list generation in QueryEditor", () => {
|
||||
expect(queryEditorSource).toContain("disabled: props.readOnly || !canCopySelectedSql.value");
|
||||
expect(contextMenuSource).toContain("disabled: state.readOnly || !canCopySelectedSql");
|
||||
expect(queryEditorSource).toMatch(/function openDelimitedListDialog\(\) \{\s+if \(props\.readOnly\) return;/);
|
||||
expect(queryEditorSource).toMatch(/function applyDelimitedListResult\(result: string\) \{[\s\S]*if \(!currentView \|\| props\.readOnly\) return;[\s\S]*replaceSelectedEditorText/);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,9 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
const contentAreaSource = readFileSync(new URL("../../../components/layout/ContentArea.vue", import.meta.url), "utf8");
|
||||
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
|
||||
const queryEditorPropsSource = readFileSync(new URL("../../../components/editor/queryEditorTypes.ts", import.meta.url), "utf8");
|
||||
const completionSource = readFileSync(new URL("../../../components/editor/useQueryEditorCompletion.ts", import.meta.url), "utf8");
|
||||
const metadataSource = readFileSync(new URL("../../../components/editor/useQueryEditorCompletionMetadata.ts", import.meta.url), "utf8");
|
||||
const sidebarRuntimeSource = readFileSync(new URL("../../../components/sidebar/SidebarTreeRuntimeHost.vue", import.meta.url), "utf8");
|
||||
const openTabsPersistenceSource = readFileSync(new URL("../../app/openTabsPersistence.ts", import.meta.url), "utf8");
|
||||
|
||||
@@ -10,16 +13,17 @@ describe("Doris external catalog completion wiring", () => {
|
||||
it("preserves the selected catalog from the sidebar query action into QueryEditor", () => {
|
||||
expect(sidebarRuntimeSource).toContain('queryStore.createTab(node.connectionId, node.database, undefined, "query", node.schema, undefined, node.catalog)');
|
||||
expect(contentAreaSource).toContain(':catalog="activeTab.catalog"');
|
||||
expect(queryEditorSource).toContain("catalog?: string;");
|
||||
expect(queryEditorSource).toContain("defineProps<QueryEditorProps>()");
|
||||
expect(queryEditorPropsSource).toContain("catalog?: string;");
|
||||
expect(openTabsPersistenceSource).toContain("catalog: tab.catalog");
|
||||
});
|
||||
|
||||
it("includes catalog scope in table and column completion requests", () => {
|
||||
expect(queryEditorSource).toMatch(/lookupLocalCompletionTables\([\s\S]*?props\.catalog/);
|
||||
expect(queryEditorSource).toMatch(/listCompletionTables\([\s\S]*?props\.catalog/);
|
||||
expect(queryEditorSource).toMatch(/listCompletionColumns\([\s\S]*?catalog\)/);
|
||||
expect(completionSource).toMatch(/lookupLocalCompletionTables\([\s\S]*?props\.catalog/);
|
||||
expect(completionSource).toMatch(/listCompletionTables\([\s\S]*?props\.catalog/);
|
||||
expect(metadataSource).toMatch(/listCompletionColumns\([\s\S]*?catalog\)/);
|
||||
expect(queryEditorSource).toMatch(/watch\(\s*\(\) => props\.catalog,[\s\S]*?refreshCompletionCache\(\)/);
|
||||
expect(queryEditorSource).toMatch(/function shouldLoadCompletionObjects[\s\S]*?if \(props\.catalog\) return false/);
|
||||
expect(queryEditorSource).toMatch(/if \(!props\.catalog && props\.databaseType !== "oracle"[\s\S]*?listCompletionObjects/);
|
||||
expect(completionSource).toMatch(/function shouldLoadCompletionObjects[\s\S]*?if \(props\.catalog\) return false/);
|
||||
expect(completionSource).toMatch(/if \(!props\.catalog && props\.databaseType !== "oracle"[\s\S]*?listCompletionObjects/);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user