feat(query-editor): enhance current statement framing
This commit is contained in:
parent
75fcd253cb
commit
18e5df6f5e
|
|
@ -73,7 +73,9 @@ 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 { normalizeSqlFormatterSettings, type SqlFormatterSettings } from "@/lib/sql/sqlFormatterConfig";
|
||||
import { currentExecutableStatementRange, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
|
||||
import { executableStatementRangeCacheForDoc, executableStatementRangeStartingAt, type ExecutableStatementRangeCache } from "@/lib/sql/executableStatementRangeCache";
|
||||
import { EMPTY_TABLE_COLUMN_TEMPLATE_DATA_TYPE, parseTableColumnTemplateFields, TABLE_COLUMN_TEMPLATE_DATABASE_TYPES } from "@/lib/table/tableColumnTemplates";
|
||||
import { buildMcpCodexConfig, buildMcpJsonConfig, buildMcpOpenCodeConfig, buildMcpVsCodeConfig, type McpEnvEntry, type McpLaunchConfig } from "@/lib/mcp/mcpConfigTemplates";
|
||||
|
|
@ -238,6 +240,7 @@ const showThemeCustomizer = ref(false);
|
|||
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 editAutoAliasTables = ref(settingsStore.editorSettings.autoAliasTables);
|
||||
const editWordWrap = ref(settingsStore.editorSettings.wordWrap);
|
||||
const editVimModeEnabled = ref(settingsStore.editorSettings.vimModeEnabled);
|
||||
|
|
@ -548,6 +551,7 @@ watch(
|
|||
editExecuteMode.value = settingsStore.editorSettings.executeMode;
|
||||
editShowExecutionTargetPicker.value = settingsStore.editorSettings.showExecutionTargetPicker;
|
||||
editShowStatementRunButtons.value = settingsStore.editorSettings.showStatementRunButtons;
|
||||
editShowCurrentStatementFrame.value = settingsStore.editorSettings.showCurrentStatementFrame;
|
||||
editAutoAliasTables.value = settingsStore.editorSettings.autoAliasTables;
|
||||
editWordWrap.value = settingsStore.editorSettings.wordWrap;
|
||||
editVimModeEnabled.value = settingsStore.editorSettings.vimModeEnabled;
|
||||
|
|
@ -641,6 +645,7 @@ function hasChanges(): boolean {
|
|||
editExecuteMode.value !== settingsStore.editorSettings.executeMode ||
|
||||
editShowExecutionTargetPicker.value !== settingsStore.editorSettings.showExecutionTargetPicker ||
|
||||
editShowStatementRunButtons.value !== settingsStore.editorSettings.showStatementRunButtons ||
|
||||
editShowCurrentStatementFrame.value !== settingsStore.editorSettings.showCurrentStatementFrame ||
|
||||
editAutoAliasTables.value !== settingsStore.editorSettings.autoAliasTables ||
|
||||
editWordWrap.value !== settingsStore.editorSettings.wordWrap ||
|
||||
editVimModeEnabled.value !== settingsStore.editorSettings.vimModeEnabled ||
|
||||
|
|
@ -701,6 +706,7 @@ async function persistSettings() {
|
|||
executeMode: editExecuteMode.value,
|
||||
showExecutionTargetPicker: editShowExecutionTargetPicker.value,
|
||||
showStatementRunButtons: editShowStatementRunButtons.value,
|
||||
showCurrentStatementFrame: editShowCurrentStatementFrame.value,
|
||||
autoAliasTables: editAutoAliasTables.value,
|
||||
wordWrap: editWordWrap.value,
|
||||
vimModeEnabled: editVimModeEnabled.value,
|
||||
|
|
@ -784,6 +790,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
|
|||
editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode;
|
||||
editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker;
|
||||
editShowStatementRunButtons.value = DEFAULT_EDITOR_SETTINGS.showStatementRunButtons;
|
||||
editShowCurrentStatementFrame.value = DEFAULT_EDITOR_SETTINGS.showCurrentStatementFrame;
|
||||
editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables;
|
||||
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
|
||||
editVimModeEnabled.value = DEFAULT_EDITOR_SETTINGS.vimModeEnabled;
|
||||
|
|
@ -854,6 +861,7 @@ function resetAllDefaults() {
|
|||
editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode;
|
||||
editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker;
|
||||
editShowStatementRunButtons.value = DEFAULT_EDITOR_SETTINGS.showStatementRunButtons;
|
||||
editShowCurrentStatementFrame.value = DEFAULT_EDITOR_SETTINGS.showCurrentStatementFrame;
|
||||
editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables;
|
||||
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
|
||||
editVimModeEnabled.value = DEFAULT_EDITOR_SETTINGS.vimModeEnabled;
|
||||
|
|
@ -2005,6 +2013,7 @@ const previewSettings = computed<{
|
|||
appPalette: AppThemePalette;
|
||||
customColors?: CustomThemeColors;
|
||||
showStatementRunButtons: boolean;
|
||||
showCurrentStatementFrame: boolean;
|
||||
}>(() => ({
|
||||
fontFamily: editFontFamily.value,
|
||||
fontSize: editFontSize.value,
|
||||
|
|
@ -2013,6 +2022,7 @@ const previewSettings = computed<{
|
|||
appPalette: themePalette.value,
|
||||
customColors: getPreviewCustomThemeColors(),
|
||||
showStatementRunButtons: editShowStatementRunButtons.value,
|
||||
showCurrentStatementFrame: editShowCurrentStatementFrame.value,
|
||||
}));
|
||||
|
||||
const previewSqlNormal = `SELECT u.id, u.name
|
||||
|
|
@ -2034,6 +2044,7 @@ let fontThemeComp: import("@codemirror/state").Compartment | null = null;
|
|||
let themeComp: import("@codemirror/state").Compartment | null = null;
|
||||
let diagnosticComp: import("@codemirror/state").Compartment | null = null;
|
||||
let previewRunGutterComp: import("@codemirror/state").Compartment | null = null;
|
||||
let currentStatementFrameComp: import("@codemirror/state").Compartment | null = null;
|
||||
let setPreviewDiagnosticsEffect: import("@codemirror/state").StateEffectType<PreviewSqlDiagnostic[]> | null = null;
|
||||
let setPreviewRunHighlightEffect: import("@codemirror/state").StateEffectType<{ from: number; to: number } | null> | null = null;
|
||||
let editorViewModule: typeof import("@codemirror/view") | null = null;
|
||||
|
|
@ -2111,6 +2122,79 @@ function handlePreviewRunGutterMouseDown(currentView: EditorViewType, line: { fr
|
|||
return true;
|
||||
}
|
||||
|
||||
function buildPreviewCurrentStatementFrameExtension(viewModule: Pick<typeof import("@codemirror/view"), "Decoration" | "EditorView" | "ViewPlugin">, enabled: boolean) {
|
||||
if (!enabled) return [];
|
||||
const { Decoration, EditorView, ViewPlugin } = 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)",
|
||||
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)",
|
||||
},
|
||||
});
|
||||
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 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, visualSqlColumns(view.state.doc.sliceString(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];
|
||||
}
|
||||
|
||||
function previewCurrentStatementFrameTo(view: import("@codemirror/view").EditorView, range: SqlTextRange): number {
|
||||
const nextChar = range.to < view.state.doc.length ? view.state.doc.sliceString(range.to, range.to + 1) : "";
|
||||
return currentStatementFrameRangeTo(nextChar, range);
|
||||
}
|
||||
|
||||
watch(
|
||||
[previewSettings, editCustomThemes, editActiveCustomThemeId],
|
||||
async ([ss]) => {
|
||||
|
|
@ -2118,7 +2202,12 @@ watch(
|
|||
|
||||
const themeExt = await loadEditorTheme(ss.theme, ss.appAppearance, ss.customColors, ss.appPalette);
|
||||
previewView.value.dispatch({
|
||||
effects: [themeComp.reconfigure(themeExt), fontThemeComp.reconfigure(editorFontTheme(editorViewModule.EditorView, ss.fontSize, ss.fontFamily)), ...(previewRunGutterComp ? [previewRunGutterComp.reconfigure(buildPreviewRunGutterExtension())] : [])],
|
||||
effects: [
|
||||
themeComp.reconfigure(themeExt),
|
||||
fontThemeComp.reconfigure(editorFontTheme(editorViewModule.EditorView, ss.fontSize, ss.fontFamily)),
|
||||
...(previewRunGutterComp ? [previewRunGutterComp.reconfigure(buildPreviewRunGutterExtension())] : []),
|
||||
...(currentStatementFrameComp ? [currentStatementFrameComp.reconfigure(buildPreviewCurrentStatementFrameExtension(editorViewModule, ss.showCurrentStatementFrame))] : []),
|
||||
],
|
||||
});
|
||||
},
|
||||
{ deep: true },
|
||||
|
|
@ -2139,6 +2228,7 @@ function cleanupPreviewEditor() {
|
|||
themeComp = null;
|
||||
diagnosticComp = null;
|
||||
previewRunGutterComp = null;
|
||||
currentStatementFrameComp = null;
|
||||
setPreviewDiagnosticsEffect = null;
|
||||
setPreviewRunHighlightEffect = null;
|
||||
editorViewModule = null;
|
||||
|
|
@ -2163,13 +2253,14 @@ watch(previewRef, async (el) => {
|
|||
previewInitialized = true;
|
||||
if (previewView.value) return;
|
||||
|
||||
const [{ EditorView, Decoration, 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 }, { EditorState, Compartment, StateEffect, StateField }, { sql, MySQL }, { basicSetup }] = await Promise.all([import("@codemirror/view"), import("@codemirror/state"), import("@codemirror/lang-sql"), import("codemirror")]);
|
||||
|
||||
editorViewModule = { EditorView } as typeof import("@codemirror/view");
|
||||
editorViewModule = { Decoration, EditorView, ViewPlugin } as typeof import("@codemirror/view");
|
||||
fontThemeComp = new Compartment();
|
||||
themeComp = new Compartment();
|
||||
diagnosticComp = new Compartment();
|
||||
previewRunGutterComp = new Compartment();
|
||||
currentStatementFrameComp = new Compartment();
|
||||
setPreviewDiagnosticsEffect = StateEffect.define<PreviewSqlDiagnostic[]>();
|
||||
setPreviewRunHighlightEffect = StateEffect.define<{ from: number; to: number } | null>();
|
||||
previewSqlDiagnostics = previewDiagnosticsForSql(currentPreviewSql());
|
||||
|
|
@ -2249,6 +2340,7 @@ watch(previewRef, async (el) => {
|
|||
themeComp.of(themeExt),
|
||||
fontThemeComp.of(editorFontTheme(EditorView, ss.fontSize, ss.fontFamily)),
|
||||
previewRunGutterComp.of(buildPreviewRunGutterExtension()),
|
||||
currentStatementFrameComp.of(buildPreviewCurrentStatementFrameExtension(editorViewModule, ss.showCurrentStatementFrame)),
|
||||
diagnosticComp.of(buildPreviewDiagnosticExtension()),
|
||||
buildPreviewRunHighlightExtension(),
|
||||
],
|
||||
|
|
@ -2394,6 +2486,14 @@ onUnmounted(cleanupPreviewEditor);
|
|||
<Switch id="editor-show-statement-run-buttons" v-model="editShowStatementRunButtons" 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-current-statement-frame">{{ t("settings.showCurrentStatementFrame") }}</Label>
|
||||
<p class="text-xs text-muted-foreground">{{ t("settings.showCurrentStatementFrameDescription") }}</p>
|
||||
</div>
|
||||
<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-word-wrap">{{ t("settings.wordWrap") }}</Label>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomC
|
|||
import { copyToClipboard } 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 { executableStatementRangeCacheForDoc, executableStatementRangeStartingAt as executableStatementRangeStartingAtLine, type ExecutableStatementRangeCache } from "@/lib/sql/executableStatementRangeCache";
|
||||
import { executableStatementRangeAtCursor, executableStatementRangeCacheForDoc, executableStatementRangeStartingAt as executableStatementRangeStartingAtLine, type ExecutableStatementRangeCache } from "@/lib/sql/executableStatementRangeCache";
|
||||
import { currentStatementFrameRangeTo, visualSqlColumns } from "@/lib/sql/currentStatementFrame";
|
||||
import { formatSqlText, type SqlFormatDialect } from "@/lib/sql/sqlFormatter";
|
||||
import { formatMongoShellText } from "@/lib/mongo/mongoFormatter";
|
||||
import { useConnectionStore, COMPLETION_METADATA_CONCURRENCY } from "@/stores/connectionStore";
|
||||
|
|
@ -282,6 +283,7 @@ const queryEditorAppearanceSettings = computed(() => {
|
|||
activeCustomThemeId: settings.activeCustomThemeId,
|
||||
wordWrap: settings.wordWrap,
|
||||
vimModeEnabled: settings.vimModeEnabled,
|
||||
showCurrentStatementFrame: settings.showCurrentStatementFrame,
|
||||
shortcuts: settings.shortcuts,
|
||||
showStatementRunButtons: settings.showStatementRunButtons,
|
||||
};
|
||||
|
|
@ -758,6 +760,12 @@ function executableStatementRangeStartingAt(currentView: EditorViewType, lineFro
|
|||
return executableStatementRangeStartingAtLine(executableStatementRangeCache, lineFrom);
|
||||
}
|
||||
|
||||
function currentExecutableStatementRange(currentView: EditorViewType): SqlTextRange | null {
|
||||
if (!supportsExecutionTargetPicker(props.databaseType)) return null;
|
||||
executableStatementRangeCache = executableStatementRangeCacheForDoc(executableStatementRangeCache, currentView.state.doc, props.databaseType);
|
||||
return executableStatementRangeAtCursor(executableStatementRangeCache, currentView.state.selection.main.head);
|
||||
}
|
||||
|
||||
function executeSqlStatementFromGutter(currentView: EditorViewType, line: { from: number; to: number }, event: Event): boolean {
|
||||
if (!(event instanceof MouseEvent) || event.button !== 0) return false;
|
||||
const statementRange = executableStatementRangeStartingAt(currentView, line.from);
|
||||
|
|
@ -2551,6 +2559,51 @@ onMounted(async () => {
|
|||
})
|
||||
: [];
|
||||
|
||||
const currentStatementFrameHighlighter = 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 (!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 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)));
|
||||
}
|
||||
|
||||
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 },
|
||||
);
|
||||
|
||||
function currentStatementFrameTo(view: import("@codemirror/view").EditorView, range: SqlTextRange): number {
|
||||
const nextChar = range.to < view.state.doc.length ? view.state.doc.sliceString(range.to, range.to + 1) : "";
|
||||
return currentStatementFrameRangeTo(nextChar, range);
|
||||
}
|
||||
|
||||
const activeLineHighlighter = ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: import("@codemirror/view").DecorationSet;
|
||||
|
|
@ -2596,6 +2649,7 @@ onMounted(async () => {
|
|||
mousedown: selectSqlLineFromGutter,
|
||||
},
|
||||
}),
|
||||
currentStatementFrameHighlighter,
|
||||
highlightActiveLineGutter(),
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
|
|
@ -2941,6 +2995,14 @@ watch(
|
|||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.databaseType,
|
||||
() => {
|
||||
executableStatementRangeCache = null;
|
||||
view.value?.dispatch({});
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.forceWordWrap,
|
||||
() => {
|
||||
|
|
@ -3219,6 +3281,31 @@ defineExpose({ openSearch, openReplace, scrollCursorIntoView, requestExecute });
|
|||
background: var(--dbx-editor-selection-background, rgba(59, 130, 246, 0.35));
|
||||
}
|
||||
|
||||
: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);
|
||||
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-run-statement-gutter) {
|
||||
min-width: 34px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2958,6 +2958,8 @@ export default {
|
|||
showExecutionTargetPickerDescription: "When enabled, running without a selection lets you choose between the current statement and all SQL.",
|
||||
showStatementRunButtons: "Show left-side run buttons",
|
||||
showStatementRunButtonsDescription: "Show per-statement run buttons in the SQL editor gutter. Keyboard shortcuts and context menu execution still work when disabled.",
|
||||
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.",
|
||||
previewStatementRunButton: "Preview statement run button",
|
||||
wordWrap: "Word wrap",
|
||||
wordWrapDescription: "Wrap long SQL lines within the editor width",
|
||||
|
|
|
|||
|
|
@ -2889,6 +2889,8 @@ export default withEnglishFallback({
|
|||
showExecutionTargetPickerDescription: "Al activarlo, ejecutar sin selección permite elegir entre la sentencia actual y todo el SQL.",
|
||||
showStatementRunButtons: "Mostrar botones de ejecución laterales",
|
||||
showStatementRunButtonsDescription: "Muestra botones para ejecutar cada sentencia en el margen del editor SQL. Los atajos de teclado y el menú contextual seguirán funcionando al desactivarlo.",
|
||||
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.",
|
||||
previewStatementRunButton: "Botón de ejecución de sentencia en vista previa",
|
||||
wordWrap: "Ajuste de línea",
|
||||
wordWrapDescription: "Ajustar las líneas largas de SQL al ancho del editor",
|
||||
|
|
|
|||
|
|
@ -2887,6 +2887,8 @@ export default withEnglishFallback({
|
|||
showExecutionTargetPickerDescription: "Se attivo, l'esecuzione senza selezione permette di scegliere tra istruzione corrente e tutto l'SQL.",
|
||||
showStatementRunButtons: "Mostra pulsanti di esecuzione laterali",
|
||||
showStatementRunButtonsDescription: "Mostra nel margine dell'editor SQL i pulsanti per eseguire ogni istruzione. Scorciatoie da tastiera e menu contestuale continuano a funzionare quando disattivati.",
|
||||
showCurrentStatementFrame: "Mostra cornice istruzione corrente",
|
||||
showCurrentStatementFrameDescription: "Se attivo, l'editor SQL disegna un contorno intorno all'istruzione eseguibile corrente; se disattivato, il contorno è nascosto.",
|
||||
previewStatementRunButton: "Pulsante di esecuzione istruzione in anteprima",
|
||||
wordWrap: "A capo automatico",
|
||||
wordWrapDescription: "Incolonna le righe SQL lunghe entro la larghezza dell'editor",
|
||||
|
|
|
|||
|
|
@ -2878,6 +2878,8 @@ export default withEnglishFallback({
|
|||
showExecutionTargetPickerDescription: "有効にすると、選択なしで実行するときに現在の文とすべてのSQLを一時的に選べます。",
|
||||
showStatementRunButtons: "左側の実行ボタンを表示",
|
||||
showStatementRunButtonsDescription: "SQLエディタのガターに文ごとの実行ボタンを表示します。無効にしてもキーボードショートカットとコンテキストメニューからの実行は引き続き使えます。",
|
||||
showCurrentStatementFrame: "現在の文の枠線を表示",
|
||||
showCurrentStatementFrameDescription: "有効にすると、SQLエディタで現在実行可能な文を枠線で示します。無効にすると枠線を表示しません。",
|
||||
previewStatementRunButton: "文の実行ボタンのプレビュー",
|
||||
wordWrap: "折り返し",
|
||||
wordWrapDescription: "長いSQL行をエディタ幅内で折り返します",
|
||||
|
|
|
|||
|
|
@ -2888,6 +2888,8 @@ export default withEnglishFallback({
|
|||
showExecutionTargetPickerDescription: "Quando ativado, executar sem seleção permite escolher entre a instrução atual e todo o SQL.",
|
||||
showStatementRunButtons: "Mostrar botões de execução laterais",
|
||||
showStatementRunButtonsDescription: "Mostra botões para executar cada instrução na margem do editor SQL. Atalhos de teclado e execução pelo menu de contexto continuam funcionando quando desativado.",
|
||||
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.",
|
||||
previewStatementRunButton: "Botão de execução de instrução na prévia",
|
||||
wordWrap: "Quebra de linha",
|
||||
wordWrapDescription: "Quebrar linhas SQL longas dentro da largura do editor",
|
||||
|
|
|
|||
|
|
@ -2958,6 +2958,8 @@ export default withEnglishFallback({
|
|||
showExecutionTargetPickerDescription: "开启后,无选区执行时可在当前语句和全部 SQL 之间临时选择。",
|
||||
showStatementRunButtons: "显示左侧执行按钮",
|
||||
showStatementRunButtonsDescription: "在 SQL 编辑器左侧显示按语句执行的快捷按钮。关闭后仍可通过快捷键和右键菜单执行。",
|
||||
showCurrentStatementFrame: "显示当前语句外框线",
|
||||
showCurrentStatementFrameDescription: "开启后,在 SQL 编辑器中用外框线标出当前可执行语句;关闭后不显示外框线。",
|
||||
previewStatementRunButton: "预览语句执行按钮",
|
||||
wordWrap: "自动换行",
|
||||
wordWrapDescription: "长 SQL 在编辑器宽度内自动折行显示",
|
||||
|
|
|
|||
|
|
@ -2785,6 +2785,8 @@ export default withEnglishFallback({
|
|||
showExecutionTargetPickerDescription: "啟用後,無選取執行時可在目前語句與全部 SQL 之間臨時選擇。",
|
||||
showStatementRunButtons: "顯示左側執行按鈕",
|
||||
showStatementRunButtonsDescription: "在 SQL 編輯器左側顯示按語句執行的快捷按鈕。關閉後仍可透過快捷鍵和右鍵選單執行。",
|
||||
showCurrentStatementFrame: "顯示目前語句外框線",
|
||||
showCurrentStatementFrameDescription: "啟用後,在 SQL 編輯器中用外框線標出目前可執行語句;關閉後不顯示外框線。",
|
||||
previewStatementRunButton: "預覽語句執行按鈕",
|
||||
wordWrap: "自動換行",
|
||||
wordWrapDescription: "長 SQL 在編輯器寬度內自動折行顯示",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { currentStatementFrameRangeTo, isWideSqlChar, visualSqlColumns } from "@/lib/sql/currentStatementFrame";
|
||||
import type { SqlTextRange } from "@/lib/sql/sqlStatementRanges";
|
||||
|
||||
describe("currentStatementFrameRangeTo", () => {
|
||||
it("includes a directly adjacent trailing semicolon in frame width calculations", () => {
|
||||
const range: SqlTextRange = { from: 0, to: "SELECT 1".length, sql: "SELECT 1" };
|
||||
expect(currentStatementFrameRangeTo(";", range)).toBe(range.to + 1);
|
||||
});
|
||||
|
||||
it("does not extend the frame when the next character is not a semicolon", () => {
|
||||
const range: SqlTextRange = { from: 0, to: "SELECT 1".length, sql: "SELECT 1" };
|
||||
expect(currentStatementFrameRangeTo("\n", range)).toBe(range.to);
|
||||
});
|
||||
});
|
||||
|
||||
describe("visualSqlColumns", () => {
|
||||
it("counts ASCII as one column, tabs as four, and CJK/fullwidth characters as two", () => {
|
||||
expect(visualSqlColumns("A\t中B")).toBe(1 + 4 + 2 + 2);
|
||||
});
|
||||
|
||||
it("recognizes common wide SQL text characters", () => {
|
||||
expect(isWideSqlChar("中")).toBe(true);
|
||||
expect(isWideSqlChar("A")).toBe(true);
|
||||
expect(isWideSqlChar("A")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { Text } from "@codemirror/state";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { executableStatementRangeCacheForDoc, executableStatementRangeStartingAt, type ExecutableStatementRangeParser } from "@/lib/sql/executableStatementRangeCache";
|
||||
import { executableStatementRangeAtCursor, executableStatementRangeCacheForDoc, executableStatementRangeStartingAt, type ExecutableStatementRangeParser } from "@/lib/sql/executableStatementRangeCache";
|
||||
|
||||
describe("executableStatementRangeCacheForDoc", () => {
|
||||
it("reuses parsed executable statement ranges for the same document and database type", () => {
|
||||
|
|
@ -27,6 +27,41 @@ describe("executableStatementRangeCacheForDoc", () => {
|
|||
expect(executableStatementRangeStartingAt(cache, secondStatementLine.from)?.sql).toBe("SELECT *\nFROM menus AS mn\nLIMIT 100");
|
||||
});
|
||||
|
||||
it("resolves the current statement from a cursor inside a continuation line", () => {
|
||||
const doc = Text.of(["SELECT *", "FROM apis AS ap", "LIMIT 100;", "", "SELECT *", "FROM menus AS mn", "LIMIT 100;"]);
|
||||
const cache = executableStatementRangeCacheForDoc(null, doc, "mysql");
|
||||
const cursor = doc.toString().indexOf("menus");
|
||||
|
||||
expect(executableStatementRangeAtCursor(cache, cursor)?.sql).toBe("SELECT *\nFROM menus AS mn\nLIMIT 100");
|
||||
});
|
||||
|
||||
it("keeps indentation and same-line semicolon gaps attached to the current statement", () => {
|
||||
const doc = Text.of(["SELECT 1;", " SELECT 2;"]);
|
||||
const cache = executableStatementRangeCacheForDoc(null, doc, "mysql");
|
||||
const indentationCursor = doc.line(2).from + 2;
|
||||
const semicolonGapCursor = doc.toString().indexOf(";") + 1;
|
||||
|
||||
expect(executableStatementRangeAtCursor(cache, indentationCursor)?.sql).toBe("SELECT 2");
|
||||
expect(executableStatementRangeAtCursor(cache, semicolonGapCursor)?.sql).toBe("SELECT 1");
|
||||
});
|
||||
|
||||
it("returns null for blank and pure comment cursor lines", () => {
|
||||
const doc = Text.of(["SELECT 1;", "-- comment", "/* block comment */", "", "SELECT 2;"]);
|
||||
const cache = executableStatementRangeCacheForDoc(null, doc, "mysql");
|
||||
|
||||
expect(executableStatementRangeAtCursor(cache, doc.line(2).from + 3)).toBeNull();
|
||||
expect(executableStatementRangeAtCursor(cache, doc.line(3).from + 3)).toBeNull();
|
||||
expect(executableStatementRangeAtCursor(cache, doc.line(4).from)).toBeNull();
|
||||
});
|
||||
|
||||
it("resolves SQL after a leading block comment on the same line", () => {
|
||||
const doc = Text.of(["/* comment */ SELECT 1;"]);
|
||||
const cache = executableStatementRangeCacheForDoc(null, doc, "mysql");
|
||||
|
||||
expect(executableStatementRangeAtCursor(cache, doc.toString().indexOf("SELECT"))?.sql).toBe("SELECT 1");
|
||||
expect(executableStatementRangeAtCursor(cache, doc.toString().indexOf("comment"))).toBeNull();
|
||||
});
|
||||
|
||||
it("rebuilds the cache when the document instance changes", () => {
|
||||
const firstDoc = Text.of(["SELECT 1;"]);
|
||||
const secondDoc = Text.of(["SELECT 1;"]);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { buildExecutionCandidates, executableStatementRanges, fullSqlRange, hasMultipleExecutionTargets, splitSqlStatementRanges, statementRangeAtCursor, supportsExecutionTargetPicker } from "@/lib/sql/sqlStatementRanges";
|
||||
import { buildExecutionCandidates, currentExecutableStatementRange, executableStatementRanges, fullSqlRange, hasMultipleExecutionTargets, splitSqlStatementRanges, statementRangeAtCursor, supportsExecutionTargetPicker } from "@/lib/sql/sqlStatementRanges";
|
||||
|
||||
function indexOf(sql: string, needle: string, occurrence = 1): number {
|
||||
let from = 0;
|
||||
|
|
@ -74,6 +74,17 @@ BEGIN
|
|||
END;
|
||||
SELECT 2;`;
|
||||
|
||||
const mysqlRoutineWithLoopsFixture = `CREATE PROCEDURE p_loop()
|
||||
BEGIN
|
||||
WHILE 1 = 0 DO
|
||||
SELECT 'while; still body';
|
||||
END WHILE;
|
||||
REPEAT
|
||||
SELECT 'repeat; still body';
|
||||
UNTIL 1 = 1 END REPEAT;
|
||||
END;
|
||||
SELECT 2;`;
|
||||
|
||||
describe("splitSqlStatementRanges", () => {
|
||||
it("splits multiple top-level statements", () => {
|
||||
const sql = "SELECT 1;\nSELECT 2;\nSELECT 3;";
|
||||
|
|
@ -149,6 +160,16 @@ describe("splitSqlStatementRanges", () => {
|
|||
expect(rangeSqlTexts(splitSqlStatementRanges(sql, "mysql"))).toEqual(["BEGIN", "INSERT INTO t VALUES (1)", "COMMIT"]);
|
||||
});
|
||||
|
||||
it("treats SQL Server GO lines as batch delimiters", () => {
|
||||
const sql = "SELECT 1\nGO\nSELECT 2;\n GO 2\nSELECT 3";
|
||||
expect(rangeSqlTexts(splitSqlStatementRanges(sql, "sqlserver"))).toEqual(["SELECT 1", "SELECT 2", "SELECT 3"]);
|
||||
});
|
||||
|
||||
it("does not treat GO inside strings or comments as a SQL Server batch delimiter", () => {
|
||||
const sql = "SELECT 'GO'\n-- GO\nSELECT 2\nGO\nSELECT 3";
|
||||
expect(rangeSqlTexts(splitSqlStatementRanges(sql, "sqlserver"))).toEqual(["SELECT 'GO'\n-- GO\nSELECT 2", "SELECT 3"]);
|
||||
});
|
||||
|
||||
it("keeps Oracle PL/SQL blocks together and treats slash lines as delimiters", () => {
|
||||
const ranges = splitSqlStatementRanges(oraclePlSqlFixture, "oracle");
|
||||
expect(rangeSqlTexts(ranges)).toEqual([oraclePlSqlFixture.slice(0, oraclePlSqlFixture.indexOf("\n/")), "SELECT 1"]);
|
||||
|
|
@ -386,6 +407,18 @@ WHERE request_json LIKE '%"paperFlag":null%';`;
|
|||
expect(range?.sql.trim()).toBe(mysqlRoutineFixture.slice(0, mysqlRoutineFixture.indexOf("\nSELECT 2;")).replace(/;$/, "").trim());
|
||||
});
|
||||
|
||||
it("returns null on SQL Server GO batch delimiter lines", () => {
|
||||
const sql = "SELECT 1\nGO\nSELECT 2";
|
||||
expect(statementRangeAtCursor(sql, indexOf(sql, "GO"), "sqlserver")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns the current SQL Server batch around GO delimiters", () => {
|
||||
const sql = "SELECT 1\nGO\nSELECT 2\nGO\nSELECT 3";
|
||||
expect(statementRangeAtCursor(sql, indexOf(sql, "1"), "sqlserver")?.sql.trim()).toBe("SELECT 1");
|
||||
expect(statementRangeAtCursor(sql, indexOf(sql, "2"), "sqlserver")?.sql.trim()).toBe("SELECT 2");
|
||||
expect(statementRangeAtCursor(sql, indexOf(sql, "3"), "sqlserver")?.sql.trim()).toBe("SELECT 3");
|
||||
});
|
||||
|
||||
it("returns the full Oracle PL/SQL block for cursors inside nested statements", () => {
|
||||
const range = statementRangeAtCursor(oraclePlSqlFixture, indexOf(oraclePlSqlFixture, "ORDERS_10K", 2), "oracle");
|
||||
expect(range?.sql.trim()).toBe(oraclePlSqlFixture.slice(0, oraclePlSqlFixture.indexOf("\n/")));
|
||||
|
|
@ -427,6 +460,42 @@ describe("executableStatementRanges", () => {
|
|||
it("does not split executable MySQL routine ranges at inner statements", () => {
|
||||
expect(rangeSqlTexts(executableStatementRanges(mysqlRoutineFixture, "mysql"))).toEqual([mysqlRoutineFixture.slice(0, mysqlRoutineFixture.indexOf("\nSELECT 2;")).replace(/;$/, "").trim(), "SELECT 2"]);
|
||||
});
|
||||
|
||||
it("does not split executable MySQL routine ranges at WHILE and REPEAT endings", () => {
|
||||
expect(rangeSqlTexts(executableStatementRanges(mysqlRoutineWithLoopsFixture, "mysql"))).toEqual([mysqlRoutineWithLoopsFixture.slice(0, mysqlRoutineWithLoopsFixture.indexOf("\nSELECT 2;")).replace(/;$/, "").trim(), "SELECT 2"]);
|
||||
});
|
||||
|
||||
it("returns executable SQL Server batches without GO delimiter lines", () => {
|
||||
expect(rangeSqlTexts(executableStatementRanges("SELECT 1\nGO\nSELECT 2;", "sqlserver"))).toEqual(["SELECT 1", "SELECT 2"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("currentExecutableStatementRange", () => {
|
||||
it("uses the current SQL statement range for multi-line DDL", () => {
|
||||
const sql = "ALTER TABLE `yb_course_order`\n ADD COLUMN `audit_status` tinyint(4) DEFAULT NULL\n COMMENT '审核状态:0-待审核,1-已通过,2-已拒绝',\n ADD COLUMN `close_reason` varchar(30) DEFAULT NULL\n COMMENT '关闭原因:timeout-超时关闭,cancel-取消关闭,refund-退款关闭';\nSELECT 1;";
|
||||
|
||||
expect(currentExecutableStatementRange(sql, indexOf(sql, "close_reason"), "mysql")?.sql.trim()).toBe(sql.slice(0, sql.indexOf(";\nSELECT")));
|
||||
});
|
||||
|
||||
it("returns null on blank and pure comment lines", () => {
|
||||
const sql = "SELECT 1;\n-- comment\n\nSELECT 2;";
|
||||
|
||||
expect(currentExecutableStatementRange(sql, indexOf(sql, "comment"), "mysql")).toBeNull();
|
||||
expect(currentExecutableStatementRange(sql, sql.indexOf("\n\n") + 1, "mysql")).toBeNull();
|
||||
});
|
||||
|
||||
it("uses the current Redis command line", () => {
|
||||
const sql = "GET user:1\n DEL user:2\n# comment";
|
||||
|
||||
expect(currentExecutableStatementRange(sql, indexOf(sql, "DEL"), "redis")?.sql).toBe("DEL user:2");
|
||||
expect(currentExecutableStatementRange(sql, indexOf(sql, "comment"), "redis")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not expose current statement framing for MongoDB", () => {
|
||||
const sql = "db.users.find({})";
|
||||
|
||||
expect(currentExecutableStatementRange(sql, indexOf(sql, "users"), "mongodb")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("fullSqlRange", () => {
|
||||
|
|
@ -526,6 +595,12 @@ describe("buildExecutionCandidates", () => {
|
|||
const candidates = buildExecutionCandidates(sql, indexOf(sql, "COUNT", 2), "mysql");
|
||||
expect(candidateSummaries(candidates)).toEqual(["cursor:select COUNT(1) FROM your_table;", "all:select COUNT(1) FROM your_table;\ndelimiter ;;\nselect COUNT(1) FROM your_table;\n\n;;\ndelimiter ;"]);
|
||||
});
|
||||
|
||||
it("uses the current SQL Server batch for cursor candidates", () => {
|
||||
const sql = "SELECT 1\nGO\nSELECT 2;";
|
||||
const candidates = buildExecutionCandidates(sql, indexOf(sql, "2"), "sqlserver");
|
||||
expect(candidateSummaries(candidates)).toEqual(["cursor:SELECT 2", "all:SELECT 1\nGO\nSELECT 2;"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasMultipleExecutionTargets", () => {
|
||||
|
|
@ -555,6 +630,10 @@ describe("hasMultipleExecutionTargets", () => {
|
|||
expect(hasMultipleExecutionTargets(mysqlRoutineFixture, "mysql")).toBe(true);
|
||||
});
|
||||
|
||||
it("counts SQL Server GO batches as multiple execution targets", () => {
|
||||
expect(hasMultipleExecutionTargets("SELECT 1\nGO\nSELECT 2", "sqlserver")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not show multiple targets for MySQL DESC UPDATE joins", () => {
|
||||
const sql = "desc update test_orders a\njoin test_users b\non a.id=b.id \nset a.name = '张三'\nwhere b.id > 10;";
|
||||
expect(hasMultipleExecutionTargets(sql, "mysql")).toBe(false);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import type { SqlTextRange } from "@/lib/sql/sqlStatementRanges";
|
||||
|
||||
export function currentStatementFrameRangeTo(nextChar: string, range: SqlTextRange): number {
|
||||
return nextChar === ";" ? range.to + 1 : range.to;
|
||||
}
|
||||
|
||||
export function visualSqlColumns(text: string): number {
|
||||
let columns = 0;
|
||||
for (const ch of text) {
|
||||
if (ch === "\t") {
|
||||
columns += 4;
|
||||
} else if (isWideSqlChar(ch)) {
|
||||
columns += 2;
|
||||
} else {
|
||||
columns += 1;
|
||||
}
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
|
||||
export function isWideSqlChar(ch: string): boolean {
|
||||
return /[\u1100-\u115f\u2329\u232a\u2e80-\u303e\u3040-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff00-\uff60\uffe0-\uffe6]/u.test(ch);
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ export interface ExecutableStatementRangeCache {
|
|||
doc: Text;
|
||||
databaseType?: DatabaseType;
|
||||
byStart: Map<number, SqlTextRange>;
|
||||
ranges: SqlTextRange[];
|
||||
}
|
||||
|
||||
export type ExecutableStatementRangeParser = (sql: string, databaseType?: DatabaseType) => SqlTextRange[];
|
||||
|
|
@ -14,12 +15,59 @@ export function executableStatementRangeCacheForDoc(cache: ExecutableStatementRa
|
|||
if (cache?.doc === doc && cache.databaseType === databaseType) return cache;
|
||||
|
||||
const byStart = new Map<number, SqlTextRange>();
|
||||
for (const range of parse(doc.toString(), databaseType)) {
|
||||
const ranges = parse(doc.toString(), databaseType);
|
||||
for (const range of ranges) {
|
||||
byStart.set(range.from, range);
|
||||
}
|
||||
return { doc, databaseType, byStart };
|
||||
return { doc, databaseType, byStart, ranges };
|
||||
}
|
||||
|
||||
export function executableStatementRangeStartingAt(cache: ExecutableStatementRangeCache, lineFrom: number): SqlTextRange | null {
|
||||
return cache.byStart.get(lineFrom) ?? null;
|
||||
}
|
||||
|
||||
export function executableStatementRangeAtCursor(cache: ExecutableStatementRangeCache, cursorPos: number): SqlTextRange | null {
|
||||
const pos = Math.max(0, Math.min(cursorPos, cache.doc.length));
|
||||
const line = cache.doc.lineAt(pos);
|
||||
const lineText = line.text.trim();
|
||||
if (!lineText || lineText.startsWith("--") || lineText.startsWith("#") || isCursorOnLeadingBlockComment(line.text, pos - line.from)) return null;
|
||||
|
||||
for (let index = 0; index < cache.ranges.length; index += 1) {
|
||||
const range = cache.ranges[index];
|
||||
if (pos >= range.from && pos <= range.to) return range;
|
||||
|
||||
if (pos < range.from && range.from <= line.to && cache.doc.sliceString(pos, range.from).trim() === "") {
|
||||
return range;
|
||||
}
|
||||
|
||||
const next = cache.ranges[index + 1];
|
||||
if (pos > range.to && (!next || pos < next.from) && range.to >= line.from && range.to <= line.to && cursorRemainsOnRangeLine(cache.doc, range.to, pos)) {
|
||||
return range;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isCursorOnLeadingBlockComment(lineText: string, lineOffset: number): boolean {
|
||||
const commentStart = lineText.search(/\S/);
|
||||
if (commentStart < 0 || !lineText.startsWith("/*", commentStart)) return false;
|
||||
|
||||
const commentEnd = lineText.indexOf("*/", commentStart + 2);
|
||||
if (commentEnd < 0) return true;
|
||||
const afterComment = lineText.slice(commentEnd + 2);
|
||||
if (!afterComment.trim()) return true;
|
||||
|
||||
return lineOffset <= commentEnd + 2;
|
||||
}
|
||||
|
||||
function cursorRemainsOnRangeLine(doc: Text, rangeTo: number, cursorPos: number): boolean {
|
||||
const between = doc.sliceString(rangeTo, cursorPos);
|
||||
if (between.includes("\n")) return false;
|
||||
const delimiterIndex = between.lastIndexOf(";");
|
||||
if (delimiterIndex === -1) return between.trim() === "";
|
||||
|
||||
const beforeDelimiter = between.slice(0, delimiterIndex);
|
||||
const afterDelimiter = between.slice(delimiterIndex + 1);
|
||||
return beforeDelimiter.trim() === "" && afterDelimiter.trim() === "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -271,6 +271,14 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
|
|||
continue;
|
||||
}
|
||||
|
||||
if (supportsSqlServerGoCommands(databaseType) && isAtLineStart(sql, i) && isSqlServerGoLine(sql, i)) {
|
||||
const lineEnd = findLineEnd(sql, i);
|
||||
flush(i);
|
||||
i = nextLineStart(sql, lineEnd);
|
||||
statementHitStart = i;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "'") {
|
||||
markContent(i);
|
||||
state = "single";
|
||||
|
|
@ -296,7 +304,7 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
|
|||
continue;
|
||||
}
|
||||
// Postgres dollar quoting: $tag$ ... $tag$ (tag may be empty, i.e. $$)
|
||||
if (ch === "$") {
|
||||
if (!customDelimiter && ch === "$") {
|
||||
const tagMatch = /^\$[A-Za-z_0-9]*\$/.exec(sql.slice(i));
|
||||
if (tagMatch) {
|
||||
markContent(i);
|
||||
|
|
@ -1386,6 +1394,15 @@ function isSlashLine(sql: string, pos: number): boolean {
|
|||
return sql.slice(pos, lineEnd).trim() === "/";
|
||||
}
|
||||
|
||||
function supportsSqlServerGoCommands(databaseType?: DatabaseType): boolean {
|
||||
return databaseType === "sqlserver";
|
||||
}
|
||||
|
||||
function isSqlServerGoLine(sql: string, pos: number): boolean {
|
||||
const lineEnd = findLineEnd(sql, pos);
|
||||
return /^go(?:\s+\d+)?$/i.test(sql.slice(pos, lineEnd).trim());
|
||||
}
|
||||
|
||||
function startsDelimiterCommand(sql: string, pos: number): boolean {
|
||||
const prefix = sql.slice(pos, pos + 9);
|
||||
return prefix.toLowerCase() === "delimiter" && (sql[pos + 9] === " " || sql[pos + 9] === "\t");
|
||||
|
|
@ -1468,9 +1485,15 @@ export function executableStatementRanges(sql: string, databaseType?: DatabaseTy
|
|||
return splitSqlStatementRanges(sql, databaseType).flatMap((statement) => splitStatementRangeAtSoftStarts(sql, statement, databaseType).map((range) => rangeFor(range, sql)));
|
||||
}
|
||||
|
||||
export function currentExecutableStatementRange(sql: string, cursorPos: number, databaseType?: DatabaseType): SqlTextRange | null {
|
||||
if (databaseType === "redis") return redisCommandRangeAtCursor(sql, cursorPos);
|
||||
if (databaseType === "mongodb") return null;
|
||||
return statementRangeAtCursor(sql, cursorPos, databaseType);
|
||||
}
|
||||
|
||||
export function buildExecutionCandidates(sql: string, cursorPos: number, databaseType?: DatabaseType): SqlExecutionCandidate[] {
|
||||
const full = fullSqlRange(sql);
|
||||
const cursorStatement = databaseType === "redis" ? redisCommandRangeAtCursor(sql, cursorPos) : statementRangeAtCursor(sql, cursorPos, databaseType);
|
||||
const cursorStatement = currentExecutableStatementRange(sql, cursorPos, databaseType);
|
||||
|
||||
if (!full && !cursorStatement) return [];
|
||||
if (!full) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,14 @@ describe("normalizeEditorSettings", () => {
|
|||
expect(normalizeEditorSettings({ autoAliasTables: false }).autoAliasTables).toBe(false);
|
||||
});
|
||||
|
||||
it("shows the current statement frame by default", () => {
|
||||
expect(normalizeEditorSettings({}).showCurrentStatementFrame).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves disabled current statement frames", () => {
|
||||
expect(normalizeEditorSettings({ showCurrentStatementFrame: false }).showCurrentStatementFrame).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps SQL semantic diagnostics in auto mode and disabled by default", () => {
|
||||
const settings = normalizeEditorSettings({});
|
||||
expect(settings.sqlSemanticDiagnosticsMode).toBe("auto");
|
||||
|
|
|
|||
|
|
@ -371,6 +371,7 @@ export interface EditorSettings {
|
|||
executeMode: "all" | "current";
|
||||
showExecutionTargetPicker: boolean;
|
||||
showStatementRunButtons: boolean;
|
||||
showCurrentStatementFrame: boolean;
|
||||
autoAliasTables: boolean;
|
||||
wordWrap: boolean;
|
||||
vimModeEnabled: boolean;
|
||||
|
|
@ -496,6 +497,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
|||
executeMode: "all",
|
||||
showExecutionTargetPicker: false,
|
||||
showStatementRunButtons: true,
|
||||
showCurrentStatementFrame: true,
|
||||
autoAliasTables: true,
|
||||
wordWrap: false,
|
||||
vimModeEnabled: false,
|
||||
|
|
@ -718,6 +720,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
|||
executeMode: settings.executeMode ?? DEFAULT_EDITOR_SETTINGS.executeMode,
|
||||
showExecutionTargetPicker: settings.showExecutionTargetPicker ?? DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker,
|
||||
showStatementRunButtons: typeof settings.showStatementRunButtons === "boolean" ? settings.showStatementRunButtons : DEFAULT_EDITOR_SETTINGS.showStatementRunButtons,
|
||||
showCurrentStatementFrame: typeof settings.showCurrentStatementFrame === "boolean" ? settings.showCurrentStatementFrame : DEFAULT_EDITOR_SETTINGS.showCurrentStatementFrame,
|
||||
autoAliasTables: settings.autoAliasTables ?? DEFAULT_EDITOR_SETTINGS.autoAliasTables,
|
||||
wordWrap: settings.wordWrap ?? DEFAULT_EDITOR_SETTINGS.wordWrap,
|
||||
vimModeEnabled: typeof settings.vimModeEnabled === "boolean" ? settings.vimModeEnabled : DEFAULT_EDITOR_SETTINGS.vimModeEnabled,
|
||||
|
|
@ -952,6 +955,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
|||
if (partial.executeMode !== undefined) editorSettings.value.executeMode = partial.executeMode;
|
||||
if (partial.showExecutionTargetPicker !== undefined) editorSettings.value.showExecutionTargetPicker = partial.showExecutionTargetPicker;
|
||||
if (partial.showStatementRunButtons !== undefined) editorSettings.value.showStatementRunButtons = partial.showStatementRunButtons === true;
|
||||
if (partial.showCurrentStatementFrame !== undefined) editorSettings.value.showCurrentStatementFrame = partial.showCurrentStatementFrame === true;
|
||||
if (partial.autoAliasTables !== undefined) editorSettings.value.autoAliasTables = partial.autoAliasTables;
|
||||
if (partial.wordWrap !== undefined) editorSettings.value.wordWrap = partial.wordWrap;
|
||||
if (partial.vimModeEnabled !== undefined) editorSettings.value.vimModeEnabled = partial.vimModeEnabled === true;
|
||||
|
|
|
|||
|
|
@ -2778,6 +2778,28 @@ SELECT 2;";
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_routine_without_delimiter_handles_loop_end_suffixes() {
|
||||
let sql = "\
|
||||
CREATE PROCEDURE p_loop()
|
||||
BEGIN
|
||||
WHILE 1 = 0 DO
|
||||
SELECT 'while; still body';
|
||||
END WHILE;
|
||||
REPEAT
|
||||
SELECT 'repeat; still body';
|
||||
UNTIL 1 = 1 END REPEAT;
|
||||
END;
|
||||
SELECT 2;";
|
||||
assert_eq!(
|
||||
split_sql_statements_for_database(sql, DatabaseType::Mysql),
|
||||
vec![
|
||||
"CREATE PROCEDURE p_loop()\nBEGIN\n WHILE 1 = 0 DO\n SELECT 'while; still body';\n END WHILE;\n REPEAT\n SELECT 'repeat; still body';\n UNTIL 1 = 1 END REPEAT;\nEND",
|
||||
"SELECT 2",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_regular_begin_transaction_still_splits_without_delimiter() {
|
||||
assert_eq!(
|
||||
|
|
|
|||
Loading…
Reference in New Issue