feat(editor): SQL editor execution button config toggle & optimization
- 在 SQL 编辑器的左侧添加每条语句的执行按钮 - 支持通过快捷键和右键菜单执行语句 - 增加多语言支持的相关文本描述
This commit is contained in:
parent
35d5e90378
commit
6d0a59a913
|
|
@ -43,7 +43,7 @@ import {
|
|||
type CustomThemeColors,
|
||||
type CustomTheme,
|
||||
} from "@/stores/settingsStore";
|
||||
import { loadEditorTheme, editorFontTheme } from "@/lib/editor/editorThemes";
|
||||
import { createRunStatementButtonDom, loadEditorTheme, editorFontTheme } from "@/lib/editor/editorThemes";
|
||||
import { formatAiModelOption } from "@/lib/ai/aiModelPresentation";
|
||||
import ThemeCustomizerDialog from "./ThemeCustomizerDialog.vue";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
|
|
@ -74,6 +74,7 @@ import { SHORTCUT_DEFINITIONS, findShortcutConflict, normalizeShortcutSettings,
|
|||
import { formatShortcutDisplay } from "@/lib/editor/shortcutDisplay";
|
||||
import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebar/sidebarTableNameDisplay";
|
||||
import { normalizeSqlFormatterSettings, type SqlFormatterSettings } from "@/lib/sql/sqlFormatterConfig";
|
||||
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";
|
||||
import { isWindows } from "@/lib/backend/platform";
|
||||
|
|
@ -236,6 +237,7 @@ const editActiveCustomThemeId = ref(settingsStore.editorSettings.activeCustomThe
|
|||
const showThemeCustomizer = ref(false);
|
||||
const editExecuteMode = ref(settingsStore.editorSettings.executeMode);
|
||||
const editShowExecutionTargetPicker = ref(settingsStore.editorSettings.showExecutionTargetPicker);
|
||||
const editShowStatementRunButtons = ref(settingsStore.editorSettings.showStatementRunButtons);
|
||||
const editAutoAliasTables = ref(settingsStore.editorSettings.autoAliasTables);
|
||||
const editWordWrap = ref(settingsStore.editorSettings.wordWrap);
|
||||
const editVimModeEnabled = ref(settingsStore.editorSettings.vimModeEnabled);
|
||||
|
|
@ -545,6 +547,7 @@ watch(
|
|||
editActiveCustomThemeId.value = settingsStore.editorSettings.activeCustomThemeId;
|
||||
editExecuteMode.value = settingsStore.editorSettings.executeMode;
|
||||
editShowExecutionTargetPicker.value = settingsStore.editorSettings.showExecutionTargetPicker;
|
||||
editShowStatementRunButtons.value = settingsStore.editorSettings.showStatementRunButtons;
|
||||
editAutoAliasTables.value = settingsStore.editorSettings.autoAliasTables;
|
||||
editWordWrap.value = settingsStore.editorSettings.wordWrap;
|
||||
editVimModeEnabled.value = settingsStore.editorSettings.vimModeEnabled;
|
||||
|
|
@ -637,6 +640,7 @@ function hasChanges(): boolean {
|
|||
editActiveCustomThemeId.value !== settingsStore.editorSettings.activeCustomThemeId ||
|
||||
editExecuteMode.value !== settingsStore.editorSettings.executeMode ||
|
||||
editShowExecutionTargetPicker.value !== settingsStore.editorSettings.showExecutionTargetPicker ||
|
||||
editShowStatementRunButtons.value !== settingsStore.editorSettings.showStatementRunButtons ||
|
||||
editAutoAliasTables.value !== settingsStore.editorSettings.autoAliasTables ||
|
||||
editWordWrap.value !== settingsStore.editorSettings.wordWrap ||
|
||||
editVimModeEnabled.value !== settingsStore.editorSettings.vimModeEnabled ||
|
||||
|
|
@ -696,6 +700,7 @@ async function persistSettings() {
|
|||
activeCustomThemeId: editActiveCustomThemeId.value,
|
||||
executeMode: editExecuteMode.value,
|
||||
showExecutionTargetPicker: editShowExecutionTargetPicker.value,
|
||||
showStatementRunButtons: editShowStatementRunButtons.value,
|
||||
autoAliasTables: editAutoAliasTables.value,
|
||||
wordWrap: editWordWrap.value,
|
||||
vimModeEnabled: editVimModeEnabled.value,
|
||||
|
|
@ -778,6 +783,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
|
|||
editFontSize.value = DEFAULT_EDITOR_SETTINGS.fontSize;
|
||||
editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode;
|
||||
editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker;
|
||||
editShowStatementRunButtons.value = DEFAULT_EDITOR_SETTINGS.showStatementRunButtons;
|
||||
editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables;
|
||||
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
|
||||
editVimModeEnabled.value = DEFAULT_EDITOR_SETTINGS.vimModeEnabled;
|
||||
|
|
@ -847,6 +853,7 @@ function resetAllDefaults() {
|
|||
editActiveCustomThemeId.value = DEFAULT_EDITOR_SETTINGS.activeCustomThemeId;
|
||||
editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode;
|
||||
editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker;
|
||||
editShowStatementRunButtons.value = DEFAULT_EDITOR_SETTINGS.showStatementRunButtons;
|
||||
editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables;
|
||||
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
|
||||
editVimModeEnabled.value = DEFAULT_EDITOR_SETTINGS.vimModeEnabled;
|
||||
|
|
@ -1997,6 +2004,7 @@ const previewSettings = computed<{
|
|||
appAppearance: AppThemeAppearance;
|
||||
appPalette: AppThemePalette;
|
||||
customColors?: CustomThemeColors;
|
||||
showStatementRunButtons: boolean;
|
||||
}>(() => ({
|
||||
fontFamily: editFontFamily.value,
|
||||
fontSize: editFontSize.value,
|
||||
|
|
@ -2004,21 +2012,36 @@ const previewSettings = computed<{
|
|||
appAppearance: isDark.value ? "dark" : "light",
|
||||
appPalette: themePalette.value,
|
||||
customColors: getPreviewCustomThemeColors(),
|
||||
showStatementRunButtons: editShowStatementRunButtons.value,
|
||||
}));
|
||||
|
||||
const previewSqlNormal = `SELECT u.id, u.name
|
||||
FROM users u
|
||||
ORDER BY u.id LIMIT 5;`;
|
||||
ORDER BY u.id LIMIT 5;
|
||||
|
||||
SELECT o.id, o.total
|
||||
FROM orders o
|
||||
WHERE o.total > 100;`;
|
||||
const previewSqlWithSyntaxError = `SELECT u.id, u.name
|
||||
FOM users u
|
||||
ORDER BY u.id LIMIT 5;`;
|
||||
ORDER BY u.id LIMIT 5;
|
||||
|
||||
SELECT o.id, o.total
|
||||
FROM orders o
|
||||
WHERE o.total > 100;`;
|
||||
|
||||
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 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;
|
||||
let previewSqlDiagnostics: PreviewSqlDiagnostic[] = [];
|
||||
let previewExecutableCache: ExecutableStatementRangeCache | null = null;
|
||||
let previewRunHighlightRange: { from: number; to: number } | null = null;
|
||||
let previewRunHighlightTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let buildPreviewRunGutterExtension: () => import("@codemirror/state").Extension = () => [];
|
||||
|
||||
function currentPreviewSql(): string {
|
||||
return editSqlSemanticDiagnosticsEnabled.value ? previewSqlWithSyntaxError : previewSqlNormal;
|
||||
|
|
@ -2041,12 +2064,53 @@ function updatePreviewSqlDiagnostics() {
|
|||
view.dispatch({ effects });
|
||||
return;
|
||||
}
|
||||
previewExecutableCache = null;
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: currentSql.length, insert: nextSql },
|
||||
effects,
|
||||
});
|
||||
}
|
||||
|
||||
function previewExecutableStatementRangeStartingAt(currentView: EditorViewType, lineFrom: number) {
|
||||
previewExecutableCache = executableStatementRangeCacheForDoc(previewExecutableCache, currentView.state.doc, "mysql");
|
||||
return executableStatementRangeStartingAt(previewExecutableCache, lineFrom);
|
||||
}
|
||||
|
||||
function clearPreviewRunHighlight() {
|
||||
previewRunHighlightRange = null;
|
||||
if (previewView.value && setPreviewRunHighlightEffect) {
|
||||
previewView.value.dispatch({ effects: setPreviewRunHighlightEffect.of(null) });
|
||||
}
|
||||
}
|
||||
|
||||
function flashPreviewRunHighlight(range: { from: number; to: number }, event: Event) {
|
||||
previewRunHighlightRange = range;
|
||||
if (previewView.value && setPreviewRunHighlightEffect) {
|
||||
previewView.value.dispatch({ effects: setPreviewRunHighlightEffect.of(range) });
|
||||
}
|
||||
if (event.target instanceof Element) {
|
||||
const marker = event.target.closest(".cm-run-statement-marker");
|
||||
marker?.classList.add("cm-run-statement-marker--executed");
|
||||
window.setTimeout(() => marker?.classList.remove("cm-run-statement-marker--executed"), 650);
|
||||
}
|
||||
if (previewRunHighlightTimer) clearTimeout(previewRunHighlightTimer);
|
||||
previewRunHighlightTimer = window.setTimeout(() => {
|
||||
previewRunHighlightTimer = null;
|
||||
clearPreviewRunHighlight();
|
||||
}, 650);
|
||||
}
|
||||
|
||||
function handlePreviewRunGutterMouseDown(currentView: EditorViewType, line: { from: number; to: number }, event: Event): boolean {
|
||||
if (!(event instanceof MouseEvent) || event.button !== 0) return false;
|
||||
const statementRange = previewExecutableStatementRangeStartingAt(currentView, line.from);
|
||||
if (!statementRange) return false;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
flashPreviewRunHighlight({ from: statementRange.from, to: statementRange.to }, event);
|
||||
currentView.focus();
|
||||
return true;
|
||||
}
|
||||
|
||||
watch(
|
||||
[previewSettings, editCustomThemes, editActiveCustomThemeId],
|
||||
async ([ss]) => {
|
||||
|
|
@ -2054,7 +2118,7 @@ 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))],
|
||||
effects: [themeComp.reconfigure(themeExt), fontThemeComp.reconfigure(editorFontTheme(editorViewModule.EditorView, ss.fontSize, ss.fontFamily)), ...(previewRunGutterComp ? [previewRunGutterComp.reconfigure(buildPreviewRunGutterExtension())] : [])],
|
||||
});
|
||||
},
|
||||
{ deep: true },
|
||||
|
|
@ -2074,9 +2138,18 @@ function cleanupPreviewEditor() {
|
|||
fontThemeComp = null;
|
||||
themeComp = null;
|
||||
diagnosticComp = null;
|
||||
previewRunGutterComp = null;
|
||||
setPreviewDiagnosticsEffect = null;
|
||||
setPreviewRunHighlightEffect = null;
|
||||
editorViewModule = null;
|
||||
previewSqlDiagnostics = [];
|
||||
previewExecutableCache = null;
|
||||
previewRunHighlightRange = null;
|
||||
buildPreviewRunGutterExtension = () => [];
|
||||
if (previewRunHighlightTimer) {
|
||||
clearTimeout(previewRunHighlightTimer);
|
||||
previewRunHighlightTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
watch(activeSettingsTab, (tab) => {
|
||||
|
|
@ -2090,13 +2163,15 @@ watch(previewRef, async (el) => {
|
|||
previewInitialized = true;
|
||||
if (previewView.value) return;
|
||||
|
||||
const [{ EditorView, Decoration }, { 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, 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");
|
||||
fontThemeComp = new Compartment();
|
||||
themeComp = new Compartment();
|
||||
diagnosticComp = new Compartment();
|
||||
previewRunGutterComp = new Compartment();
|
||||
setPreviewDiagnosticsEffect = StateEffect.define<PreviewSqlDiagnostic[]>();
|
||||
setPreviewRunHighlightEffect = StateEffect.define<{ from: number; to: number } | null>();
|
||||
previewSqlDiagnostics = previewDiagnosticsForSql(currentPreviewSql());
|
||||
|
||||
const ss = previewSettings.value;
|
||||
|
|
@ -2132,9 +2207,51 @@ watch(previewRef, async (el) => {
|
|||
return [field, diagnosticTheme];
|
||||
};
|
||||
|
||||
class PreviewRunStatementGutterMarker extends GutterMarker {
|
||||
toDOM() {
|
||||
return createRunStatementButtonDom(t("settings.previewStatementRunButton"));
|
||||
}
|
||||
}
|
||||
|
||||
const previewRunMarker = new PreviewRunStatementGutterMarker();
|
||||
buildPreviewRunGutterExtension = () =>
|
||||
editShowStatementRunButtons.value
|
||||
? gutter({
|
||||
class: "cm-run-statement-gutter",
|
||||
lineMarker(currentView, line) {
|
||||
return previewExecutableStatementRangeStartingAt(currentView, line.from) ? previewRunMarker : null;
|
||||
},
|
||||
domEventHandlers: {
|
||||
mousedown: handlePreviewRunGutterMouseDown,
|
||||
},
|
||||
})
|
||||
: [];
|
||||
|
||||
const buildPreviewRunHighlightExtension = () => {
|
||||
const highlightEffect = setPreviewRunHighlightEffect;
|
||||
const buildDecorations = () => (previewRunHighlightRange ? Decoration.set([Decoration.mark({ class: "cm-settings-preview-run-highlight" }).range(previewRunHighlightRange.from, previewRunHighlightRange.to)], true) : Decoration.none);
|
||||
const field = StateField.define({
|
||||
create: buildDecorations,
|
||||
update(value, transaction) {
|
||||
const highlightChanged = !!highlightEffect && transaction.effects.some((effect) => effect.is(highlightEffect));
|
||||
return transaction.docChanged || highlightChanged ? buildDecorations() : value;
|
||||
},
|
||||
provide: (field) => EditorView.decorations.from(field),
|
||||
});
|
||||
return field;
|
||||
};
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: currentPreviewSql(),
|
||||
extensions: [basicSetup, sql({ dialect: MySQL }), themeComp.of(themeExt), fontThemeComp.of(editorFontTheme(EditorView, ss.fontSize, ss.fontFamily)), diagnosticComp.of(buildPreviewDiagnosticExtension())],
|
||||
extensions: [
|
||||
basicSetup,
|
||||
sql({ dialect: MySQL }),
|
||||
themeComp.of(themeExt),
|
||||
fontThemeComp.of(editorFontTheme(EditorView, ss.fontSize, ss.fontFamily)),
|
||||
previewRunGutterComp.of(buildPreviewRunGutterExtension()),
|
||||
diagnosticComp.of(buildPreviewDiagnosticExtension()),
|
||||
buildPreviewRunHighlightExtension(),
|
||||
],
|
||||
});
|
||||
|
||||
previewView.value = new EditorView({ state, parent: previewRef.value });
|
||||
|
|
@ -2269,6 +2386,14 @@ onUnmounted(cleanupPreviewEditor);
|
|||
<Switch id="editor-show-execution-target-picker" v-model="editShowExecutionTargetPicker" 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-statement-run-buttons">{{ t("settings.showStatementRunButtons") }}</Label>
|
||||
<p class="text-xs text-muted-foreground">{{ t("settings.showStatementRunButtonsDescription") }}</p>
|
||||
</div>
|
||||
<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-word-wrap">{{ t("settings.wordWrap") }}</Label>
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ import {
|
|||
type QueryEditorTableReferenceDropDetail,
|
||||
type QueryEditorTableReferencePayload,
|
||||
} from "@/lib/editor/queryEditorTableDrop";
|
||||
import { EDITOR_FONT_FAMILY_CSS_VAR, EDITOR_FONT_SIZE_CSS_VAR, loadEditorTheme, editorFontTheme, sqlCompletionTheme } from "@/lib/editor/editorThemes";
|
||||
import { EDITOR_FONT_FAMILY_CSS_VAR, EDITOR_FONT_SIZE_CSS_VAR, createRunStatementButtonDom, loadEditorTheme, editorFontTheme, sqlCompletionTheme } from "@/lib/editor/editorThemes";
|
||||
import { clampEditorFontSize, createEditorZoomCommitScheduler, fontSizeFromGestureScale, fontSizeFromWheelDelta } from "@/lib/editor/editorZoom";
|
||||
import { normalizeShortcutSettings, shortcutToCodeMirrorKey } from "@/lib/editor/shortcutRegistry";
|
||||
import { trimmedSelectionLayer } from "@/lib/editor/codemirrorTrimmedSelectionLayer";
|
||||
|
|
@ -199,6 +199,7 @@ let codeMirrorTheme: import("@codemirror/state").Compartment | null = null;
|
|||
let wordWrapComp: import("@codemirror/state").Compartment | null = null;
|
||||
let vimModeComp: import("@codemirror/state").Compartment | null = null;
|
||||
let readOnlyComp: import("@codemirror/state").Compartment | null = null;
|
||||
let runGutterComp: import("@codemirror/state").Compartment | null = null;
|
||||
let runKeymapComp: import("@codemirror/state").Compartment | null = null;
|
||||
let completionComp: import("@codemirror/state").Compartment | null = null;
|
||||
let diagnosticComp: import("@codemirror/state").Compartment | null = null;
|
||||
|
|
@ -231,6 +232,7 @@ let setSqlDiagnosticsEffect: import("@codemirror/state").StateEffectType<SqlSema
|
|||
let setPreviewRangeEffect: import("@codemirror/state").StateEffectType<{ from: number; to: number } | null> | null = null;
|
||||
let previewRangeComp: import("@codemirror/state").Compartment | null = null;
|
||||
let buildPreviewRangeExtension: (() => import("@codemirror/state").Extension) | null = null;
|
||||
let buildRunStatementGutterExtension: (() => import("@codemirror/state").Extension) | null = null;
|
||||
let indentComp: import("@codemirror/state").Compartment | null = null;
|
||||
let codeMirrorIndentUnit: typeof import("@codemirror/language").indentUnit | null = null;
|
||||
let semanticDiagnostics: SqlSemanticDiagnostic[] = [];
|
||||
|
|
@ -278,6 +280,7 @@ const queryEditorAppearanceSettings = computed(() => {
|
|||
wordWrap: settings.wordWrap,
|
||||
vimModeEnabled: settings.vimModeEnabled,
|
||||
shortcuts: settings.shortcuts,
|
||||
showStatementRunButtons: settings.showStatementRunButtons,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -2269,6 +2272,7 @@ onMounted(async () => {
|
|||
wordWrapComp = new Compartment();
|
||||
vimModeComp = new Compartment();
|
||||
readOnlyComp = new Compartment();
|
||||
runGutterComp = new Compartment();
|
||||
runKeymapComp = new Compartment();
|
||||
completionComp = new Compartment();
|
||||
diagnosticComp = new Compartment();
|
||||
|
|
@ -2388,17 +2392,23 @@ onMounted(async () => {
|
|||
|
||||
class RunStatementGutterMarker extends GutterMarker {
|
||||
toDOM() {
|
||||
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;
|
||||
return createRunStatementButtonDom("Execute statement");
|
||||
}
|
||||
}
|
||||
|
||||
const executableStatementMarker = new RunStatementGutterMarker();
|
||||
buildRunStatementGutterExtension = () =>
|
||||
settingsStore.editorSettings.showStatementRunButtons
|
||||
? gutter({
|
||||
class: "cm-run-statement-gutter",
|
||||
lineMarker(currentView, line) {
|
||||
return executableStatementRangeStartingAt(currentView, line.from) ? executableStatementMarker : null;
|
||||
},
|
||||
domEventHandlers: {
|
||||
mousedown: executeSqlStatementFromGutter,
|
||||
},
|
||||
})
|
||||
: [];
|
||||
|
||||
const activeLineHighlighter = ViewPlugin.fromClass(
|
||||
class {
|
||||
|
|
@ -2439,15 +2449,7 @@ onMounted(async () => {
|
|||
return { dom };
|
||||
},
|
||||
}),
|
||||
gutter({
|
||||
class: "cm-run-statement-gutter",
|
||||
lineMarker(currentView, line) {
|
||||
return executableStatementRangeStartingAt(currentView, line.from) ? executableStatementMarker : null;
|
||||
},
|
||||
domEventHandlers: {
|
||||
mousedown: executeSqlStatementFromGutter,
|
||||
},
|
||||
}),
|
||||
runGutterComp.of(buildRunStatementGutterExtension()),
|
||||
lineNumbers({
|
||||
domEventHandlers: {
|
||||
mousedown: selectSqlLineFromGutter,
|
||||
|
|
@ -2816,7 +2818,7 @@ function getCurrentCustomThemeColors() {
|
|||
watch(
|
||||
[queryEditorAppearanceSettings, () => isDark.value, () => themePalette.value],
|
||||
async ([ss]) => {
|
||||
if (!view.value || !codeMirrorTheme || !fontThemeComp || !wordWrapComp || !vimModeComp || !runKeymapComp || !editorViewModule) {
|
||||
if (!view.value || !codeMirrorTheme || !fontThemeComp || !wordWrapComp || !vimModeComp || !runGutterComp || !runKeymapComp || !editorViewModule) {
|
||||
return;
|
||||
}
|
||||
if (!isGestureZooming.value && !zoomCommitScheduler.hasPendingCommit() && liveFontSize.value !== ss.fontSize) {
|
||||
|
|
@ -2825,7 +2827,7 @@ watch(
|
|||
syncEditorFontCssVars(liveFontSize.value, ss.fontFamily);
|
||||
const themeColors = getCurrentCustomThemeColors();
|
||||
const [themeExt] = await Promise.all([loadEditorTheme(ss.theme, editorThemeAppearance(), themeColors, themePalette.value), ss.vimModeEnabled ? ensureCodeMirrorVim() : Promise.resolve(false)]);
|
||||
if (!view.value || !codeMirrorTheme || !wordWrapComp || !vimModeComp || !runKeymapComp || !editorViewModule) {
|
||||
if (!view.value || !codeMirrorTheme || !wordWrapComp || !vimModeComp || !runGutterComp || !runKeymapComp || !editorViewModule) {
|
||||
return;
|
||||
}
|
||||
view.value.dispatch({
|
||||
|
|
@ -2833,6 +2835,7 @@ watch(
|
|||
codeMirrorTheme.reconfigure(themeExt),
|
||||
wordWrapComp.reconfigure(props.forceWordWrap || ss.wordWrap ? editorViewModule.EditorView.lineWrapping : []),
|
||||
vimModeComp.reconfigure(vimModeExtension(settingsStore.editorSettings.vimModeEnabled)),
|
||||
runGutterComp.reconfigure(buildRunStatementGutterExtension?.() ?? []),
|
||||
runKeymapComp.reconfigure(runKeymapExtension(editorViewModule.keymap)),
|
||||
],
|
||||
});
|
||||
|
|
@ -3075,10 +3078,12 @@ defineExpose({ openSearch, openReplace, scrollCursorIntoView, requestExecute });
|
|||
}
|
||||
|
||||
:deep(.cm-run-statement-gutter .cm-gutterElement) {
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-width: 34px;
|
||||
padding: 0 5px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
:deep(.cm-run-statement-marker) {
|
||||
|
|
@ -3086,8 +3091,8 @@ defineExpose({ openSearch, openReplace, scrollCursorIntoView, requestExecute });
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: min(24px, calc(var(--dbx-editor-font-size, 13px) * 1.6));
|
||||
height: min(24px, calc(var(--dbx-editor-font-size, 13px) * 1.6));
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 1px solid transparent;
|
||||
|
|
@ -3125,8 +3130,8 @@ defineExpose({ openSearch, openReplace, scrollCursorIntoView, requestExecute });
|
|||
|
||||
:deep(.cm-run-statement-marker svg) {
|
||||
display: block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
width: min(14px, 70%);
|
||||
height: min(14px, 70%);
|
||||
pointer-events: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2943,6 +2943,9 @@ export default {
|
|||
executeModeCurrent: "Execute statement at cursor",
|
||||
showExecutionTargetPicker: "Show execution target picker",
|
||||
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.",
|
||||
previewStatementRunButton: "Preview statement run button",
|
||||
wordWrap: "Word wrap",
|
||||
wordWrapDescription: "Wrap long SQL lines within the editor width",
|
||||
vimMode: "Vim mode",
|
||||
|
|
|
|||
|
|
@ -2887,6 +2887,9 @@ export default withEnglishFallback({
|
|||
executeModeCurrent: "Ejecutar sentencia en el cursor",
|
||||
showExecutionTargetPicker: "Mostrar selector de objetivo",
|
||||
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.",
|
||||
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",
|
||||
vimMode: "Modo Vim",
|
||||
|
|
|
|||
|
|
@ -2885,6 +2885,9 @@ export default withEnglishFallback({
|
|||
executeModeCurrent: "Esegui istruzione al cursore",
|
||||
showExecutionTargetPicker: "Mostra selettore destinazione",
|
||||
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.",
|
||||
previewStatementRunButton: "Pulsante di esecuzione istruzione in anteprima",
|
||||
wordWrap: "A capo automatico",
|
||||
wordWrapDescription: "Incolonna le righe SQL lunghe entro la larghezza dell'editor",
|
||||
vimMode: "Modalita Vim",
|
||||
|
|
|
|||
|
|
@ -2876,6 +2876,9 @@ export default withEnglishFallback({
|
|||
executeModeCurrent: "カーソル位置の文を実行",
|
||||
showExecutionTargetPicker: "実行対象ピッカーを表示",
|
||||
showExecutionTargetPickerDescription: "有効にすると、選択なしで実行するときに現在の文とすべてのSQLを一時的に選べます。",
|
||||
showStatementRunButtons: "左側の実行ボタンを表示",
|
||||
showStatementRunButtonsDescription: "SQLエディタのガターに文ごとの実行ボタンを表示します。無効にしてもキーボードショートカットとコンテキストメニューからの実行は引き続き使えます。",
|
||||
previewStatementRunButton: "文の実行ボタンのプレビュー",
|
||||
wordWrap: "折り返し",
|
||||
wordWrapDescription: "長いSQL行をエディタ幅内で折り返します",
|
||||
vimMode: "Vimモード",
|
||||
|
|
|
|||
|
|
@ -2886,6 +2886,9 @@ export default withEnglishFallback({
|
|||
executeModeCurrent: "Executar instrução no cursor",
|
||||
showExecutionTargetPicker: "Mostrar seletor de destino",
|
||||
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.",
|
||||
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",
|
||||
vimMode: "Modo Vim",
|
||||
|
|
|
|||
|
|
@ -2943,6 +2943,9 @@ export default withEnglishFallback({
|
|||
executeModeCurrent: "执行光标所在语句",
|
||||
showExecutionTargetPicker: "显示执行目标选择器",
|
||||
showExecutionTargetPickerDescription: "开启后,无选区执行时可在当前语句和全部 SQL 之间临时选择。",
|
||||
showStatementRunButtons: "显示左侧执行按钮",
|
||||
showStatementRunButtonsDescription: "在 SQL 编辑器左侧显示按语句执行的快捷按钮。关闭后仍可通过快捷键和右键菜单执行。",
|
||||
previewStatementRunButton: "预览语句执行按钮",
|
||||
wordWrap: "自动换行",
|
||||
wordWrapDescription: "长 SQL 在编辑器宽度内自动折行显示",
|
||||
vimMode: "Vim 模式",
|
||||
|
|
|
|||
|
|
@ -2783,6 +2783,9 @@ export default withEnglishFallback({
|
|||
executeModeCurrent: "執行指標所在語句",
|
||||
showExecutionTargetPicker: "顯示執行目標選擇器",
|
||||
showExecutionTargetPickerDescription: "啟用後,無選取執行時可在目前語句與全部 SQL 之間臨時選擇。",
|
||||
showStatementRunButtons: "顯示左側執行按鈕",
|
||||
showStatementRunButtonsDescription: "在 SQL 編輯器左側顯示按語句執行的快捷按鈕。關閉後仍可透過快捷鍵和右鍵選單執行。",
|
||||
previewStatementRunButton: "預覽語句執行按鈕",
|
||||
wordWrap: "自動換行",
|
||||
wordWrapDescription: "長 SQL 在編輯器寬度內自動折行顯示",
|
||||
vimMode: "Vim 模式",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,16 @@ export const EDITOR_FONT_SIZE_CSS_VAR = "--dbx-editor-font-size";
|
|||
export const EDITOR_FONT_FAMILY_CSS_VAR = "--dbx-editor-font-family";
|
||||
const EDITOR_SELECTION_BACKGROUND_CSS_VAR = "--dbx-editor-selection-background";
|
||||
|
||||
export function createRunStatementButtonDom(ariaLabel = "Execute statement"): HTMLButtonElement {
|
||||
const marker = document.createElement("button");
|
||||
marker.className = "cm-run-statement-marker cm-run-statement-marker--active";
|
||||
marker.setAttribute("type", "button");
|
||||
marker.setAttribute("aria-label", ariaLabel);
|
||||
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 SUPPORTS_COLOR_MIX = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("color", "color-mix(in oklch, black 50%, white)");
|
||||
const SUPPORTS_OKLCH = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("color", "oklch(0.62 0.19 255)");
|
||||
|
||||
|
|
@ -738,6 +748,70 @@ export function buildEditorFontThemeRules(opts?: { fixedHeight?: boolean; scroll
|
|||
paddingRight: "16px",
|
||||
userSelect: "none",
|
||||
},
|
||||
".cm-run-statement-gutter": {
|
||||
minWidth: "34px",
|
||||
},
|
||||
".cm-run-statement-gutter .cm-gutterElement": {
|
||||
alignItems: "center",
|
||||
boxSizing: "border-box",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
minWidth: "34px",
|
||||
padding: "0 5px",
|
||||
},
|
||||
".cm-run-statement-marker": {
|
||||
alignItems: "center",
|
||||
background: "transparent",
|
||||
border: "1px solid transparent",
|
||||
borderRadius: "6px",
|
||||
boxSizing: "border-box",
|
||||
color: "transparent",
|
||||
display: "inline-flex",
|
||||
flexShrink: "0",
|
||||
height: `min(24px, calc(var(${EDITOR_FONT_SIZE_CSS_VAR}, ${defaults?.size ?? 13}px) * 1.6))`,
|
||||
justifyContent: "center",
|
||||
margin: "0",
|
||||
outline: "none",
|
||||
padding: "0",
|
||||
transition: "color 0.15s, background-color 0.15s",
|
||||
userSelect: "none",
|
||||
verticalAlign: "middle",
|
||||
whiteSpace: "nowrap",
|
||||
width: `min(24px, calc(var(${EDITOR_FONT_SIZE_CSS_VAR}, ${defaults?.size ?? 13}px) * 1.6))`,
|
||||
},
|
||||
".cm-run-statement-marker--active": {
|
||||
background: "rgb(16 185 129 / 0.1)",
|
||||
color: "rgb(4 120 87)",
|
||||
cursor: "pointer",
|
||||
},
|
||||
".cm-run-statement-marker--active:hover": {
|
||||
background: "rgb(16 185 129 / 0.2)",
|
||||
color: "rgb(6 95 70)",
|
||||
},
|
||||
"&.cm-editor .cm-run-statement-marker svg": {
|
||||
display: "block",
|
||||
flexShrink: "0",
|
||||
height: "min(14px, 70%)",
|
||||
pointerEvents: "none",
|
||||
width: "min(14px, 70%)",
|
||||
},
|
||||
"&.cm-editor.cm-focused .cm-run-statement-marker:focus-visible": {
|
||||
outline: "1px solid var(--ring)",
|
||||
outlineOffset: "1px",
|
||||
},
|
||||
"&.cm-editor .cm-run-statement-marker--executed": {
|
||||
background: "rgb(16 185 129 / 0.18)",
|
||||
color: "rgb(6 95 70)",
|
||||
},
|
||||
"&.cm-editor .cm-settings-preview-run-highlight": {
|
||||
background: "rgb(16 185 129 / 0.12)",
|
||||
},
|
||||
".dark &.cm-editor .cm-run-statement-marker--active": {
|
||||
color: "rgb(110 231 183)",
|
||||
},
|
||||
".dark &.cm-editor .cm-run-statement-marker--active:hover, .dark &.cm-editor .cm-run-statement-marker--executed": {
|
||||
color: "rgb(167 243 208)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -370,6 +370,7 @@ export interface EditorSettings {
|
|||
activeCustomThemeId: string;
|
||||
executeMode: "all" | "current";
|
||||
showExecutionTargetPicker: boolean;
|
||||
showStatementRunButtons: boolean;
|
||||
autoAliasTables: boolean;
|
||||
wordWrap: boolean;
|
||||
vimModeEnabled: boolean;
|
||||
|
|
@ -494,6 +495,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
|||
activeCustomThemeId: "default",
|
||||
executeMode: "all",
|
||||
showExecutionTargetPicker: false,
|
||||
showStatementRunButtons: true,
|
||||
autoAliasTables: true,
|
||||
wordWrap: false,
|
||||
vimModeEnabled: false,
|
||||
|
|
@ -715,6 +717,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
|||
activeCustomThemeId: settings.activeCustomThemeId ?? "default",
|
||||
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,
|
||||
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,
|
||||
|
|
@ -948,6 +951,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.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;
|
||||
|
|
|
|||
|
|
@ -129,6 +129,13 @@ test("defaults dangerous SQL confirmation to enabled", () => {
|
|||
assert.equal(normalizeEditorSettings({ confirmDangerousSqlExecution: false }).confirmDangerousSqlExecution, false);
|
||||
});
|
||||
|
||||
test("defaults statement run buttons to enabled and preserves saved booleans", () => {
|
||||
assert.equal(DEFAULT_EDITOR_SETTINGS.showStatementRunButtons, true);
|
||||
assert.equal(normalizeEditorSettings({}).showStatementRunButtons, true);
|
||||
assert.equal(normalizeEditorSettings({ showStatementRunButtons: false }).showStatementRunButtons, false);
|
||||
assert.equal(normalizeEditorSettings({ showStatementRunButtons: "nope" as any }).showStatementRunButtons, true);
|
||||
});
|
||||
|
||||
test("defaults unsaved SQL close confirmation to enabled", () => {
|
||||
assert.equal(DEFAULT_EDITOR_SETTINGS.confirmUnsavedSqlClose, true);
|
||||
assert.equal(normalizeEditorSettings({}).confirmUnsavedSqlClose, true);
|
||||
|
|
|
|||
Loading…
Reference in New Issue