perf(editor): cache executable statement ranges for large SQL scrolling
Co-authored-by: staff <staff@qimaos-MacBook-Pro.local>
This commit is contained in:
parent
8162a420e3
commit
ccb91784b3
|
|
@ -10,7 +10,8 @@ import SqlExecutionTargetPicker from "./SqlExecutionTargetPicker.vue";
|
|||
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
|
||||
import { copyToClipboard } from "@/lib/clipboard";
|
||||
import { resolveExecutableSql, type SqlExecutionSnapshot, type SqlExecutionOverride, type SqlExecutionCandidate } from "@/lib/sqlExecutionTarget";
|
||||
import { buildExecutionCandidates, executableStatementRanges, hasMultipleExecutionTargets, supportsExecutionTargetPicker, type SqlTextRange } from "@/lib/sqlStatementRanges";
|
||||
import { buildExecutionCandidates, hasMultipleExecutionTargets, supportsExecutionTargetPicker, type SqlTextRange } from "@/lib/sqlStatementRanges";
|
||||
import { executableStatementRangeCacheForDoc, executableStatementRangeStartingAt as executableStatementRangeStartingAtLine, type ExecutableStatementRangeCache } from "@/lib/executableStatementRangeCache";
|
||||
import { formatSqlText, type SqlFormatDialect } from "@/lib/sqlFormatter";
|
||||
import { formatMongoShellText } from "@/lib/mongoFormatter";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
|
|
@ -213,6 +214,7 @@ let editorIsActive = true;
|
|||
let tableReferenceDropListenerRegistered = false;
|
||||
let imeCompositionActive = false;
|
||||
let pendingImeModelEmit = false;
|
||||
let executableStatementRangeCache: ExecutableStatementRangeCache | null = null;
|
||||
const tableNavigationHoverClass = "query-editor--table-navigation-hover";
|
||||
|
||||
function editorThemeAppearance() {
|
||||
|
|
@ -512,7 +514,8 @@ function openTableDdlFromContextMenu() {
|
|||
}
|
||||
|
||||
function executableStatementRangeStartingAt(currentView: EditorViewType, lineFrom: number) {
|
||||
return executableStatementRanges(currentView.state.doc.toString(), props.databaseType).find((range) => range.from === lineFrom) ?? null;
|
||||
executableStatementRangeCache = executableStatementRangeCacheForDoc(executableStatementRangeCache, currentView.state.doc, props.databaseType);
|
||||
return executableStatementRangeStartingAtLine(executableStatementRangeCache, lineFrom);
|
||||
}
|
||||
|
||||
function executeSqlStatementFromGutter(currentView: EditorViewType, line: { from: number; to: number }, event: Event): boolean {
|
||||
|
|
@ -2050,25 +2053,18 @@ onMounted(async () => {
|
|||
const theme = await loadEditorTheme(initialSettings.theme, editorThemeAppearance(), getCurrentCustomThemeColors());
|
||||
|
||||
class RunStatementGutterMarker extends GutterMarker {
|
||||
constructor(private readonly isExecutable: boolean) {
|
||||
super();
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
const marker = document.createElement(this.isExecutable ? "button" : "span");
|
||||
marker.className = this.isExecutable ? "cm-run-statement-marker cm-run-statement-marker--active" : "cm-run-statement-marker";
|
||||
if (this.isExecutable) {
|
||||
marker.setAttribute("type", "button");
|
||||
marker.setAttribute("aria-label", "Execute statement");
|
||||
}
|
||||
const marker = document.createElement("button");
|
||||
marker.className = "cm-run-statement-marker cm-run-statement-marker--active";
|
||||
marker.setAttribute("type", "button");
|
||||
marker.setAttribute("aria-label", "Execute statement");
|
||||
marker.innerHTML =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z"></path></svg>';
|
||||
return marker;
|
||||
}
|
||||
}
|
||||
|
||||
const executableStatementMarker = new RunStatementGutterMarker(true);
|
||||
const inactiveStatementMarker = new RunStatementGutterMarker(false);
|
||||
const executableStatementMarker = new RunStatementGutterMarker();
|
||||
|
||||
const activeLineHighlighter = ViewPlugin.fromClass(
|
||||
class {
|
||||
|
|
@ -2112,7 +2108,7 @@ onMounted(async () => {
|
|||
gutter({
|
||||
class: "cm-run-statement-gutter",
|
||||
lineMarker(currentView, line) {
|
||||
return executableStatementRangeStartingAt(currentView, line.from) ? executableStatementMarker : inactiveStatementMarker;
|
||||
return executableStatementRangeStartingAt(currentView, line.from) ? executableStatementMarker : null;
|
||||
},
|
||||
domEventHandlers: {
|
||||
mousedown: executeSqlStatementFromGutter,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import { Text } from "@codemirror/state";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { executableStatementRangeCacheForDoc, executableStatementRangeStartingAt, type ExecutableStatementRangeParser } from "@/lib/executableStatementRangeCache";
|
||||
|
||||
describe("executableStatementRangeCacheForDoc", () => {
|
||||
it("reuses parsed executable statement ranges for the same document and database type", () => {
|
||||
const doc = Text.of(["SELECT 1;", "SELECT 2;"]);
|
||||
const parse = vi.fn<ExecutableStatementRangeParser>(() => [
|
||||
{ from: 0, to: 8, sql: "SELECT 1" },
|
||||
{ from: 10, to: 18, sql: "SELECT 2" },
|
||||
]);
|
||||
|
||||
const first = executableStatementRangeCacheForDoc(null, doc, "mysql", parse);
|
||||
const second = executableStatementRangeCacheForDoc(first, doc, "mysql", parse);
|
||||
|
||||
expect(second).toBe(first);
|
||||
expect(parse).toHaveBeenCalledTimes(1);
|
||||
expect(executableStatementRangeStartingAt(second, 10)?.sql).toBe("SELECT 2");
|
||||
});
|
||||
|
||||
it("rebuilds the cache when the document instance changes", () => {
|
||||
const firstDoc = Text.of(["SELECT 1;"]);
|
||||
const secondDoc = Text.of(["SELECT 1;"]);
|
||||
const parse = vi.fn<ExecutableStatementRangeParser>(() => [{ from: 0, to: 8, sql: "SELECT 1" }]);
|
||||
|
||||
const first = executableStatementRangeCacheForDoc(null, firstDoc, "mysql", parse);
|
||||
const second = executableStatementRangeCacheForDoc(first, secondDoc, "mysql", parse);
|
||||
|
||||
expect(second).not.toBe(first);
|
||||
expect(parse).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("rebuilds the cache when the database type changes", () => {
|
||||
const doc = Text.of(["SELECT 1;"]);
|
||||
const parse = vi.fn<ExecutableStatementRangeParser>(() => [{ from: 0, to: 8, sql: "SELECT 1" }]);
|
||||
|
||||
const mysql = executableStatementRangeCacheForDoc(null, doc, "mysql", parse);
|
||||
const postgres = executableStatementRangeCacheForDoc(mysql, doc, "postgres", parse);
|
||||
|
||||
expect(postgres).not.toBe(mysql);
|
||||
expect(parse).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import type { Text } from "@codemirror/state";
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
import { executableStatementRanges, type SqlTextRange } from "@/lib/sqlStatementRanges";
|
||||
|
||||
export interface ExecutableStatementRangeCache {
|
||||
doc: Text;
|
||||
databaseType?: DatabaseType;
|
||||
byStart: Map<number, SqlTextRange>;
|
||||
}
|
||||
|
||||
export type ExecutableStatementRangeParser = (sql: string, databaseType?: DatabaseType) => SqlTextRange[];
|
||||
|
||||
export function executableStatementRangeCacheForDoc(cache: ExecutableStatementRangeCache | null, doc: Text, databaseType?: DatabaseType, parse: ExecutableStatementRangeParser = executableStatementRanges): ExecutableStatementRangeCache {
|
||||
if (cache?.doc === doc && cache.databaseType === databaseType) return cache;
|
||||
|
||||
const byStart = new Map<number, SqlTextRange>();
|
||||
for (const range of parse(doc.toString(), databaseType)) {
|
||||
byStart.set(range.from, range);
|
||||
}
|
||||
return { doc, databaseType, byStart };
|
||||
}
|
||||
|
||||
export function executableStatementRangeStartingAt(cache: ExecutableStatementRangeCache, lineFrom: number): SqlTextRange | null {
|
||||
return cache.byStart.get(lineFrom) ?? null;
|
||||
}
|
||||
Loading…
Reference in New Issue