fix(editor): rebuild current SQL statement frame with pixel-accurate layer
This commit is contained in:
1 parent
7c2528d32f
commit
a4129c02fa
7 files changed
+397
-269
No files matched your search
@@ -95,7 +95,8 @@ import { eventToModifierOnlyShortcut, eventToShortcut } from "@/lib/editor/keybo
|
||||
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, visualSqlColumnsWithInlineHints } from "@/lib/sql/currentStatementFrame";
|
||||
import { currentStatementFrameRangeTo } from "@/lib/sql/currentStatementFrame";
|
||||
import { currentStatementFrameLayer } from "@/lib/editor/codemirrorCurrentStatementFrameLayer";
|
||||
import { normalizeSqlFormatterSettings, type SqlFormatterSettings } from "@/lib/sql/sqlFormatterConfig";
|
||||
import { validateConfigName, generateId, type AiConfigItem, type ConfigNameValidationResult } from "@/lib/ai/aiConfigList";
|
||||
import { currentExecutableStatementRange, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
|
||||
@@ -3211,79 +3212,28 @@ function handlePreviewRunGutterMouseDown(currentView: EditorViewType, line: { fr
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildPreviewCurrentStatementFrameExtension(viewModule: Pick<typeof import("@codemirror/view"), "Decoration" | "EditorView" | "ViewPlugin">, enabled: boolean) {
|
||||
function buildPreviewCurrentStatementFrameExtension(viewModule: Pick<typeof import("@codemirror/view"), "EditorView" | "layer" | "RectangleMarker">, enabled: boolean) {
|
||||
if (!enabled) return [];
|
||||
const { Decoration, EditorView, ViewPlugin } = viewModule;
|
||||
const { EditorView } = viewModule;
|
||||
const frameTheme = EditorView.baseTheme({
|
||||
".cm-db-current-statement-line": {
|
||||
position: "relative",
|
||||
},
|
||||
".cm-db-current-statement-line::after": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: "0",
|
||||
bottom: "0",
|
||||
left: "0",
|
||||
boxSizing: "border-box",
|
||||
width: "var(--dbx-current-statement-frame-width, 100%)",
|
||||
borderRight: "1px solid rgb(34 197 94 / 0.75)",
|
||||
borderLeft: "1px solid rgb(34 197 94 / 0.75)",
|
||||
".cm-db-currentStatementFrameLayer": {
|
||||
pointerEvents: "none",
|
||||
},
|
||||
".cm-db-current-statement-line--first::after": {
|
||||
borderTop: "1px solid rgb(34 197 94 / 0.75)",
|
||||
},
|
||||
".cm-db-current-statement-line--last::after": {
|
||||
borderBottom: "1px solid rgb(34 197 94 / 0.75)",
|
||||
".cm-db-currentStatementFrame": {
|
||||
boxSizing: "border-box",
|
||||
border: "1px solid rgb(34 197 94 / 0.75)",
|
||||
borderRadius: "2px",
|
||||
pointerEvents: "none",
|
||||
},
|
||||
});
|
||||
const framePlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: import("@codemirror/view").DecorationSet;
|
||||
constructor(view: import("@codemirror/view").EditorView) {
|
||||
this.decorations = this.getDeco(view);
|
||||
}
|
||||
update(update: import("@codemirror/view").ViewUpdate) {
|
||||
this.decorations = this.getDeco(update.view);
|
||||
}
|
||||
getDeco(view: import("@codemirror/view").EditorView) {
|
||||
if (view.state.selection.ranges.some((range) => !range.empty)) return Decoration.none;
|
||||
const range = currentExecutableStatementRange(view.state.doc.toString(), view.state.selection.main.head, "mysql");
|
||||
if (!range) return Decoration.none;
|
||||
const frameLayer = currentStatementFrameLayer({ layer: viewModule.layer, RectangleMarker: viewModule.RectangleMarker }, (view) => {
|
||||
if (view.state.selection.ranges.some((range) => !range.empty)) return null;
|
||||
const range = currentExecutableStatementRange(view.state.doc.toString(), view.state.selection.main.head, "mysql");
|
||||
if (!range) return null;
|
||||
return { from: range.from, to: previewCurrentStatementFrameTo(view, range) };
|
||||
});
|
||||
|
||||
const startLine = view.state.doc.lineAt(range.from);
|
||||
const frameTo = previewCurrentStatementFrameTo(view, range);
|
||||
const endLine = view.state.doc.lineAt(Math.max(range.from, frameTo - 1));
|
||||
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, visualSqlColumnsWithInlineHints(view.state.doc.sliceString(line.from, lineRangeTo), line.from, lineRangeTo));
|
||||
}
|
||||
|
||||
const deco: any[] = [];
|
||||
const frameWidth = `calc(${maxWidth}ch + 2ch)`;
|
||||
for (let lineNumber = startLine.number; lineNumber <= endLine.number; lineNumber += 1) {
|
||||
const line = view.state.doc.line(lineNumber);
|
||||
const classes = ["cm-db-current-statement-line"];
|
||||
if (lineNumber === startLine.number) classes.push("cm-db-current-statement-line--first");
|
||||
if (lineNumber === endLine.number) classes.push("cm-db-current-statement-line--last");
|
||||
deco.push(
|
||||
Decoration.line({
|
||||
class: classes.join(" "),
|
||||
attributes: {
|
||||
style: `--dbx-current-statement-frame-width: ${frameWidth};`,
|
||||
},
|
||||
}).range(line.from),
|
||||
);
|
||||
}
|
||||
return Decoration.set(deco);
|
||||
}
|
||||
},
|
||||
{ decorations: (v) => v.decorations },
|
||||
);
|
||||
|
||||
return [framePlugin, frameTheme];
|
||||
return [frameLayer, frameTheme];
|
||||
}
|
||||
|
||||
function previewCurrentStatementFrameTo(view: import("@codemirror/view").EditorView, range: SqlTextRange): number {
|
||||
@@ -3348,12 +3298,19 @@ watch(previewRef, async (el) => {
|
||||
previewInitialized = true;
|
||||
if (previewView.value) return;
|
||||
|
||||
const [{ EditorView, Decoration, ViewPlugin, gutter, GutterMarker }, { EditorState, Compartment, StateEffect, StateField }, { sql, MySQL }, { basicSetup }] = await Promise.all([import("@codemirror/view"), import("@codemirror/state"), import("@codemirror/lang-sql"), import("codemirror")]);
|
||||
const [{ EditorView, Decoration, ViewPlugin, gutter, GutterMarker, layer, RectangleMarker }, { EditorState, Compartment, StateEffect, StateField }, { sql, MySQL }, { basicSetup }] = await Promise.all([
|
||||
import("@codemirror/view"),
|
||||
import("@codemirror/state"),
|
||||
import("@codemirror/lang-sql"),
|
||||
import("codemirror"),
|
||||
]);
|
||||
|
||||
editorViewModule = {
|
||||
Decoration,
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
layer,
|
||||
RectangleMarker,
|
||||
} as typeof import("@codemirror/view");
|
||||
fontThemeComp = new Compartment();
|
||||
themeComp = new Compartment();
|
||||
|
||||
@@ -15,8 +15,8 @@ import { completionMatchRanges } from "@/lib/common/completionMatch";
|
||||
import { executionCandidateForMode, 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, shouldRebuildCurrentStatementFrame, visualSqlColumnsWithInlineHints } from "@/lib/sql/currentStatementFrame";
|
||||
import { expandToSqlStatementWindow, parseInsertValueHints } from "@/lib/sql/insertValueHints";
|
||||
import { currentStatementFrameRangeTo } from "@/lib/sql/currentStatementFrame";
|
||||
import { expandToSqlStatementWindow } from "@/lib/sql/insertValueHints";
|
||||
import { insertValueHintColumnNames } from "@/lib/sql/insertValueHintColumns";
|
||||
import { formatSqlForEditing, compressSqlText, type SqlFormatDialect } from "@/lib/sql/sqlFormatter";
|
||||
import { detectAndFormatStructured } from "@/lib/sql/autoFormat";
|
||||
@@ -101,6 +101,7 @@ import { completionLabelPresentation } from "@/lib/editor/sqlCompletionPresentat
|
||||
import { clampEditorFontSize, createEditorZoomCommitScheduler, fontSizeFromGestureScale, fontSizeFromWheelDelta } from "@/lib/editor/editorZoom";
|
||||
import { normalizeShortcutSettings, shortcutToCodeMirrorKey } from "@/lib/editor/shortcutRegistry";
|
||||
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 { focusEditorView } from "@/lib/editor/queryEditorFocus";
|
||||
@@ -3927,7 +3928,29 @@ onMounted(async () => {
|
||||
})();
|
||||
|
||||
const [
|
||||
{ EditorView, keymap, rectangularSelection, hoverTooltip, showTooltip, closeHoverTooltips, Decoration, tooltips, gutter, GutterMarker, lineNumberMarkers, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, crosshairCursor, scrollPastEnd, ViewPlugin },
|
||||
{
|
||||
EditorView,
|
||||
keymap,
|
||||
rectangularSelection,
|
||||
hoverTooltip,
|
||||
showTooltip,
|
||||
closeHoverTooltips,
|
||||
Decoration,
|
||||
tooltips,
|
||||
gutter,
|
||||
GutterMarker,
|
||||
lineNumberMarkers,
|
||||
lineNumbers,
|
||||
highlightActiveLineGutter,
|
||||
highlightSpecialChars,
|
||||
drawSelection,
|
||||
dropCursor,
|
||||
crosshairCursor,
|
||||
scrollPastEnd,
|
||||
ViewPlugin,
|
||||
layer,
|
||||
RectangleMarker,
|
||||
},
|
||||
{ EditorState, EditorSelection, Compartment, Prec, RangeSet, StateEffect, StateField },
|
||||
langSql,
|
||||
{ autocompletion, startCompletion, acceptCompletion, closeBrackets, closeBracketsKeymap, snippetCompletion, completionStatus, completionKeymap, insertCompletionText, nextSnippetField, closeCompletion },
|
||||
@@ -4253,74 +4276,43 @@ onMounted(async () => {
|
||||
await ensureCodeMirrorVim();
|
||||
}
|
||||
|
||||
const currentStatementFrameHighlighter = ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: import("@codemirror/view").DecorationSet;
|
||||
private configuration: string;
|
||||
constructor(view: import("@codemirror/view").EditorView) {
|
||||
this.configuration = this.currentConfiguration();
|
||||
this.decorations = this.getDeco(view);
|
||||
}
|
||||
update(update: import("@codemirror/view").ViewUpdate) {
|
||||
const configuration = this.currentConfiguration();
|
||||
if (!shouldRebuildCurrentStatementFrame({ docChanged: update.docChanged, selectionSet: update.selectionSet, configurationChanged: configuration !== this.configuration })) return;
|
||||
this.configuration = configuration;
|
||||
this.decorations = this.getDeco(update.view);
|
||||
}
|
||||
currentConfiguration() {
|
||||
return `${settingsStore.editorSettings.showCurrentStatementFrame}:${settingsStore.editorSettings.showInsertValueHints}:${props.databaseType ?? ""}`;
|
||||
}
|
||||
getDeco(view: import("@codemirror/view").EditorView) {
|
||||
if (!settingsStore.editorSettings.showCurrentStatementFrame) return Decoration.none;
|
||||
if (view.state.selection.ranges.some((range) => !range.empty)) return Decoration.none;
|
||||
const range = currentExecutableStatementRange(view);
|
||||
if (!range) return Decoration.none;
|
||||
|
||||
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" && props.databaseType !== "easysearch" && props.databaseType !== "victoriametrics") {
|
||||
const statementSql = view.state.doc.sliceString(range.from, range.to);
|
||||
if (/\binsert\b/i.test(statementSql)) {
|
||||
insertValueHints = parseInsertValueHints(statementSql, { resolveTableColumns: getInsertValueHintTableColumns }).map((hint) => ({
|
||||
...hint,
|
||||
from: hint.from + range.from,
|
||||
}));
|
||||
const currentStatementFrameExtension = currentStatementFrameLayer({ layer, RectangleMarker }, (view) => {
|
||||
if (!settingsStore.editorSettings.showCurrentStatementFrame) return null;
|
||||
if (view.state.selection.ranges.some((range) => !range.empty)) return null;
|
||||
let range = currentExecutableStatementRange(view);
|
||||
if (!range) {
|
||||
const cursorPos = view.state.selection.main.head;
|
||||
const cursorLine = view.state.doc.lineAt(cursorPos);
|
||||
executableStatementRangeCache = executableStatementRangeCacheForDoc(executableStatementRangeCache, view.state.doc, props.databaseType, sqlStatementParameterOptions());
|
||||
// Find ranges that overlap the cursor line, then expand to include
|
||||
// adjacent ranges (handles parser fragments from edge cases like
|
||||
// ultra-long comments splitting a statement).
|
||||
let mergedFrom = cursorLine.from;
|
||||
let mergedTo = cursorLine.from;
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (const cachedRange of executableStatementRangeCache.ranges) {
|
||||
// Only merge ranges that overlap the cursor line or are adjacent
|
||||
// to the current merged region
|
||||
if (cachedRange.from <= mergedTo && cachedRange.to >= mergedFrom) {
|
||||
const newFrom = Math.min(mergedFrom, cachedRange.from);
|
||||
const newTo = Math.max(mergedTo, cachedRange.to);
|
||||
if (newFrom !== mergedFrom || newTo !== mergedTo) {
|
||||
mergedFrom = newFrom;
|
||||
mergedTo = newTo;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
} 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, visualSqlColumnsWithInlineHints(view.state.doc.sliceString(line.from, lineRangeTo), line.from, lineRangeTo, insertValueHints));
|
||||
}
|
||||
|
||||
const deco: any[] = [];
|
||||
const frameWidth = `calc(${maxWidth}ch + 2ch)`;
|
||||
for (let lineNumber = startLine.number; lineNumber <= endLine.number; lineNumber += 1) {
|
||||
const line = view.state.doc.line(lineNumber);
|
||||
const classes = ["cm-db-current-statement-line"];
|
||||
if (lineNumber === startLine.number) classes.push("cm-db-current-statement-line--first");
|
||||
if (lineNumber === endLine.number) classes.push("cm-db-current-statement-line--last");
|
||||
deco.push(
|
||||
Decoration.line({
|
||||
class: classes.join(" "),
|
||||
attributes: {
|
||||
style: `--dbx-current-statement-frame-width: ${frameWidth};`,
|
||||
},
|
||||
}).range(line.from),
|
||||
);
|
||||
}
|
||||
return Decoration.set(deco);
|
||||
}
|
||||
},
|
||||
{ decorations: (v) => v.decorations },
|
||||
);
|
||||
if (mergedTo > mergedFrom) {
|
||||
range = { from: mergedFrom, to: mergedTo, sql: view.state.doc.sliceString(mergedFrom, mergedTo) };
|
||||
}
|
||||
}
|
||||
if (!range) return null;
|
||||
return { from: range.from, to: currentStatementFrameTo(view, range) };
|
||||
});
|
||||
|
||||
function currentStatementFrameTo(view: import("@codemirror/view").EditorView, range: SqlTextRange): number {
|
||||
return currentStatementFrameRangeTo(view.state.doc, range);
|
||||
@@ -4374,7 +4366,7 @@ onMounted(async () => {
|
||||
mousedown: selectSqlLineFromGutter,
|
||||
},
|
||||
}),
|
||||
currentStatementFrameHighlighter,
|
||||
currentStatementFrameExtension,
|
||||
highlightActiveLineGutter(),
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
@@ -5299,29 +5291,15 @@ defineExpose({
|
||||
color: rgb(216 180 254) !important;
|
||||
}
|
||||
|
||||
:deep(.cm-db-current-statement-line) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.cm-db-current-statement-line::after) {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
width: var(--dbx-current-statement-frame-width, 100%);
|
||||
border-right: 1px solid rgb(34 197 94 / 0.75);
|
||||
border-left: 1px solid rgb(34 197 94 / 0.75);
|
||||
:deep(.cm-db-currentStatementFrameLayer) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.cm-db-current-statement-line--first::after) {
|
||||
border-top: 1px solid rgb(34 197 94 / 0.75);
|
||||
}
|
||||
|
||||
:deep(.cm-db-current-statement-line--last::after) {
|
||||
border-bottom: 1px solid rgb(34 197 94 / 0.75);
|
||||
:deep(.cm-db-currentStatementFrame) {
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgb(34 197 94 / 0.75);
|
||||
border-radius: 2px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.cm-run-statement-gutter) {
|
||||
|
||||
Reference in new issue
Block a user