feat(editor): preview dml changes as readonly select
This commit is contained in:
1 parent
d59b86bd57
commit
333c3a3f44
25 files changed
+848
-4
No files matched your search
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, onActivated, onDeactivated, watch, shallowRef, computed, nextTick } from "vue";
|
||||
import { AlignLeft, Camera, CaseLower, CaseUpper, ClipboardPaste, Code2, Download, FileCode, MessageSquareText, Minimize2, Pencil, PencilRuler, Play, Copy, List, Scissors, Search, Sparkles, Table2, TextSelect, Trash2 } from "@lucide/vue";
|
||||
import { AlignLeft, Camera, CaseLower, CaseUpper, ClipboardPaste, Code2, Download, Eye, FileCode, MessageSquareText, Minimize2, Pencil, PencilRuler, Play, Copy, List, Scissors, Search, Sparkles, Table2, TextSelect, Trash2 } from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { Completion, CompletionContext } from "@codemirror/autocomplete";
|
||||
import { Transaction, StateEffect } from "@codemirror/state";
|
||||
@@ -18,6 +18,7 @@ import { executionCandidateForMode, resolveExecutableSql, type SqlExecutionSnaps
|
||||
import { buildExecutionCandidates, hasMultipleExecutionTargets, supportsExecutionTargetPicker, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
|
||||
import { executableStatementRangeAtCursor, executableStatementRangeCacheForDoc, executableStatementRangeStartingAt as executableStatementRangeStartingAtLine, type ExecutableStatementRangeCache } from "@/lib/sql/executableStatementRangeCache";
|
||||
import { currentStatementFrameRangeTo } from "@/lib/sql/currentStatementFrame";
|
||||
import { looksLikeDmlStatement } from "@/lib/sql/dmlChangePreview";
|
||||
import { expandToSqlStatementWindow } from "@/lib/sql/insertValueHints";
|
||||
import { insertValueHintColumnNames } from "@/lib/sql/insertValueHintColumns";
|
||||
import { canFormatSqlForDatabaseType, formatSqlForDisplay, formatSqlForEditing, compressSqlText, sqlFormatDialectForDbType, type SqlFormatDialect } from "@/lib/sql/sqlFormatter";
|
||||
@@ -223,6 +224,7 @@ const emit = defineEmits<{
|
||||
"update:modelValue": [value: string];
|
||||
selectionChange: [value: string];
|
||||
cursorChange: [pos: number];
|
||||
previewChangesAvailable: [value: boolean];
|
||||
formatError: [message: string];
|
||||
execute: [source: SqlExecutionOverride];
|
||||
executeInNewResultTab: [source: SqlExecutionOverride];
|
||||
@@ -364,6 +366,7 @@ const isGestureZooming = ref(false);
|
||||
const searchPanelRef = ref<InstanceType<typeof EditorSearchPanel>>();
|
||||
const selectedSql = ref("");
|
||||
const executableSql = ref("");
|
||||
const previewContextSql = ref("");
|
||||
const contextObjectTarget = ref<SqlObjectNavigationTarget | null>(null);
|
||||
|
||||
interface SelectStarExpansionTarget {
|
||||
@@ -1066,9 +1069,10 @@ function insertLineBelow(currentView: EditorViewType): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function syncContextMenuState(currentView: EditorViewType, starPosition?: number) {
|
||||
function syncContextMenuState(currentView: EditorViewType, starPosition?: number, previewPosition?: number) {
|
||||
selectedSql.value = selectedSqlFromView(currentView);
|
||||
executableSql.value = executableSqlFromView(currentView);
|
||||
previewContextSql.value = resolvePreviewDmlCandidate(previewPosition);
|
||||
selectStarExpansionTarget.value = selectStarExpansionTargetForView(currentView, starPosition);
|
||||
}
|
||||
|
||||
@@ -1152,7 +1156,8 @@ function selectStarExpansionTargetForView(currentView: EditorViewType, position?
|
||||
|
||||
function syncContextMenuStateAtEvent(currentView: EditorViewType, event: MouseEvent) {
|
||||
const pos = currentView.posAtCoords({ x: event.clientX, y: event.clientY });
|
||||
syncContextMenuState(currentView, pos ?? undefined);
|
||||
// 预览按“右键点击处”解析当前语句(执行按光标处),右键处与光标一致时才直觉一致。
|
||||
syncContextMenuState(currentView, pos ?? undefined, pos ?? undefined);
|
||||
if (pos == null) {
|
||||
contextObjectTarget.value = null;
|
||||
return;
|
||||
@@ -1424,6 +1429,61 @@ function exportQueryFromContextMenu(format: "csv" | "xlsx" | "txt") {
|
||||
emit("exportQuery", { sql, format, columnComments: undefined });
|
||||
}
|
||||
|
||||
// 与「执行」使用同一套候选解析:选区优先,否则取 position(右键点击处)/ 光标处的单条语句。
|
||||
// 注意:不跟随 executeAllOnBlankLine 回退到“整篇文档”(那会包含多条语句)。
|
||||
function resolvePreviewDmlCandidate(position?: number): string {
|
||||
const currentView = view.value;
|
||||
if (!currentView) return "";
|
||||
const selection = currentView.state.selection.main;
|
||||
if (!selection.empty) {
|
||||
const text = currentView.state.sliceDoc(selection.from, selection.to);
|
||||
return looksLikeDmlStatement(text) ? text : "";
|
||||
}
|
||||
const doc = currentView.state.doc.toString();
|
||||
const cursorPos = position ?? selection.head;
|
||||
const parameterOptions = sqlStatementParameterOptions();
|
||||
const candidates = buildExecutionCandidates(doc, cursorPos, props.databaseType, parameterOptions);
|
||||
const cursorCandidate = candidates.find((item) => item.supportedKinds.includes("cursor"));
|
||||
return cursorCandidate && looksLikeDmlStatement(cursorCandidate.sql) ? cursorCandidate.sql : "";
|
||||
}
|
||||
|
||||
// 「预览变更」:把当前 DML 语句改写为只读 SELECT,作为新结果标签执行(干跑,不写库)。
|
||||
async function requestPreviewChanges(stackSql?: string) {
|
||||
let sql = (stackSql ?? "").trim();
|
||||
// 永远只预览“单条语句”:禁用整篇文档回退(那会包含多条语句)。
|
||||
if (!sql) sql = resolvePreviewDmlCandidate();
|
||||
if (!sql) {
|
||||
toast(t("editor.previewChangesNoStatement"), 3000);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const identifierQuote = props.connectionId ? connectionStore.connectionIdentifierQuote?.(props.connectionId) : undefined;
|
||||
// 第一次:生成基础预览 SELECT,并拿到目标表引用。
|
||||
let preview = await api.buildDmlChangePreviewSql({ sql, databaseType: props.databaseType, identifierQuote });
|
||||
// 单表 UPDATE:拉取目标表列元数据,让「新值」列紧跟其原值列(交错展开)。
|
||||
if (preview.tables.length === 1 && props.connectionId && props.database) {
|
||||
const tableRef = preview.tables[0];
|
||||
if (tableRef.table) {
|
||||
const columns = await api
|
||||
.getColumns(props.connectionId, props.database, tableRef.schema ?? "", tableRef.table, tableRef.catalog)
|
||||
.then((infos) => infos.map((column) => column.name))
|
||||
.catch(() => undefined);
|
||||
if (columns?.length) {
|
||||
preview = await api.buildDmlChangePreviewSql({ sql, databaseType: props.databaseType, identifierQuote, columns });
|
||||
}
|
||||
}
|
||||
}
|
||||
// 前置注释标注干跑预览(引擎会忽略注释),并在新结果标签中展示受影响行 + 新值列。
|
||||
emit("executeInNewResultTab", `/* ${t("editor.previewChangesComment", { operation: preview.operation })} */\n${preview.sql}`);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
// http 层抛 BackendErrorException(Error),tauri 层拒绝时是 String。
|
||||
const message = error instanceof Error ? error.message : typeof error === "string" ? error : t("editor.previewChangesFailed");
|
||||
toast(message, 4000);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function copySelectedSqlFromContextMenu() {
|
||||
if (!canCopySelectedSql.value) return;
|
||||
try {
|
||||
@@ -1776,6 +1836,12 @@ const contextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
icon: Play,
|
||||
shortcut: shortcuts.executeSqlInNewResultTab,
|
||||
},
|
||||
{
|
||||
label: t("editor.previewChanges"),
|
||||
action: () => void requestPreviewChanges(previewContextSql.value),
|
||||
disabled: !previewContextSql.value,
|
||||
icon: Eye,
|
||||
},
|
||||
{
|
||||
label: t("editor.contextMenu.export"),
|
||||
icon: Download,
|
||||
@@ -5769,6 +5835,7 @@ onMounted(async () => {
|
||||
syncContextMenuState(update.view);
|
||||
emit("selectionChange", selectedSqlFromView(update.view));
|
||||
emit("cursorChange", update.state.selection.main.head);
|
||||
emit("previewChangesAvailable", !!previewContextSql.value);
|
||||
latestSelection = readEditorSelection(update.view);
|
||||
if (editorIsActive) emitEditorSelection(latestSelection);
|
||||
}
|
||||
@@ -6114,6 +6181,7 @@ onMounted(async () => {
|
||||
|
||||
restoreEditorViewport();
|
||||
syncContextMenuState(view.value);
|
||||
emit("previewChangesAvailable", !!previewContextSql.value);
|
||||
syncEditorFontCssVars(liveFontSize.value, initialSettings.fontFamily);
|
||||
syncEditorDiagnosticCssVars();
|
||||
registerTableReferenceDropListener();
|
||||
@@ -6602,6 +6670,7 @@ defineExpose({
|
||||
shouldBlockExecutionShortcut,
|
||||
requestExecute,
|
||||
requestExecuteInNewResultTab,
|
||||
requestPreviewChanges,
|
||||
captureExecutionSnapshot,
|
||||
pasteClipboardAsSqlInCondition,
|
||||
focusStatementRange,
|
||||
|
||||
Reference in new issue
Block a user