mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-29 00:09:52 +00:00
feat: format sql (#19)
Co-authored-by: SuLe <sule@SuLedeMacBook-Air.local>
This commit is contained in:
+51
-1
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { DatabaseZap, FilePlus2, Play, Loader2, X, Globe, Moon, Sun, Upload, Download, Plus, History, Server, Table2, Database, Search, ShieldCheck, Sparkles, Pin } from "lucide-vue-next";
|
||||
import { DatabaseZap, FilePlus2, Play, Loader2, X, Globe, Moon, Sun, Upload, Download, Plus, History, Server, Table2, Database, Search, ShieldCheck, Sparkles, Pin, AlignLeft } from "lucide-vue-next";
|
||||
import { Splitpanes, Pane } from "splitpanes";
|
||||
import "splitpanes/dist/splitpanes.css";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -34,6 +34,7 @@ import { getCurrentWindow, type Theme } from "@tauri-apps/api/window";
|
||||
import { getCurrentWebview } from "@tauri-apps/api/webview";
|
||||
import * as api from "@/lib/tauri";
|
||||
import { resolveExecutableSql } from "@/lib/sqlExecutionTarget";
|
||||
import type { SqlFormatDialect } from "@/lib/sqlFormatter";
|
||||
|
||||
const { t } = useI18n();
|
||||
const connectionStore = useConnectionStore();
|
||||
@@ -47,6 +48,7 @@ const showHistory = ref(false);
|
||||
const dangerSql = ref("");
|
||||
const pendingDangerSql = ref("");
|
||||
const selectedSql = ref("");
|
||||
const formatSqlRequestId = ref(0);
|
||||
const showDangerDialog = ref(false);
|
||||
const databaseOptions = ref<Record<string, string[]>>({});
|
||||
const loadingDatabaseOptions = ref<Record<string, boolean>>({});
|
||||
@@ -84,6 +86,25 @@ const activeConnection = computed(() => {
|
||||
return tab ? connectionStore.getConfig(tab.connectionId) : undefined;
|
||||
});
|
||||
|
||||
const activeSqlFormatDialect = computed<SqlFormatDialect>(() => {
|
||||
switch (activeConnection.value?.db_type) {
|
||||
case "mysql":
|
||||
return "mysql";
|
||||
case "postgres":
|
||||
return "postgres";
|
||||
case "sqlite":
|
||||
return "sqlite";
|
||||
case "sqlserver":
|
||||
return "sqlserver";
|
||||
default:
|
||||
return "generic";
|
||||
}
|
||||
});
|
||||
|
||||
const editorDialect = computed<"mysql" | "postgres">(() =>
|
||||
activeConnection.value?.db_type === "postgres" ? "postgres" : "mysql"
|
||||
);
|
||||
|
||||
const activeTabContext = computed(() => {
|
||||
const tab = activeTab.value;
|
||||
const connection = activeConnection.value;
|
||||
@@ -208,6 +229,16 @@ function onEditorSelectionChange(val: string) {
|
||||
selectedSql.value = val;
|
||||
}
|
||||
|
||||
function formatActiveSql() {
|
||||
const tab = activeTab.value;
|
||||
if (!tab || tab.mode !== "query" || !tab.sql.trim()) return;
|
||||
formatSqlRequestId.value++;
|
||||
}
|
||||
|
||||
function onFormatSqlError() {
|
||||
toast(t("toolbar.formatSqlFailed"));
|
||||
}
|
||||
|
||||
function newQuery() {
|
||||
if (!connectionStore.activeConnectionId) return;
|
||||
const conn = connectionStore.connections.find(
|
||||
@@ -511,6 +542,21 @@ async function setupFileDrop() {
|
||||
<TooltipContent>{{ t('toolbar.executeShortcut') }}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
:disabled="!activeTab || activeTab.mode !== 'query' || activeTab.isExecuting || !activeTab.sql.trim()"
|
||||
@click="formatActiveSql"
|
||||
>
|
||||
<AlignLeft class="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t('toolbar.formatSql') }}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div class="flex-1" />
|
||||
|
||||
<Tooltip>
|
||||
@@ -689,8 +735,12 @@ async function setupFileDrop() {
|
||||
<QueryEditor
|
||||
class="flex-1"
|
||||
:model-value="activeTab.sql"
|
||||
:dialect="editorDialect"
|
||||
:format-dialect="activeSqlFormatDialect"
|
||||
:format-request-id="formatSqlRequestId"
|
||||
@update:model-value="onEditorUpdate"
|
||||
@selection-change="onEditorSelectionChange"
|
||||
@format-error="onFormatSqlError"
|
||||
@execute="tryExecute"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,15 +2,19 @@
|
||||
import { ref, onMounted, onBeforeUnmount, watch, shallowRef } from "vue";
|
||||
import type { EditorView as EditorViewType } from "@codemirror/view";
|
||||
import { resolveExecutableSql } from "@/lib/sqlExecutionTarget";
|
||||
import { formatSqlText, type SqlFormatDialect } from "@/lib/sqlFormatter";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string;
|
||||
dialect?: "mysql" | "postgres";
|
||||
formatDialect?: SqlFormatDialect;
|
||||
formatRequestId?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: string];
|
||||
"selectionChange": [value: string];
|
||||
"formatError": [message: string];
|
||||
execute: [sql: string];
|
||||
}>();
|
||||
|
||||
@@ -69,6 +73,31 @@ function executableSqlFromView(currentView: EditorViewType): string {
|
||||
return resolveExecutableSql(currentView.state.doc.toString(), selectedSqlFromView(currentView));
|
||||
}
|
||||
|
||||
async function formatCurrentSql() {
|
||||
const currentView = view.value;
|
||||
if (!currentView) return;
|
||||
|
||||
const selection = currentView.state.selection.main;
|
||||
const formatsSelection = !selection.empty;
|
||||
const from = formatsSelection ? selection.from : 0;
|
||||
const to = formatsSelection ? selection.to : currentView.state.doc.length;
|
||||
const source = currentView.state.sliceDoc(from, to);
|
||||
if (!source.trim()) return;
|
||||
|
||||
try {
|
||||
const formatted = await formatSqlText(source, props.formatDialect ?? props.dialect ?? "generic");
|
||||
if (formatted === source) return;
|
||||
currentView.dispatch({
|
||||
changes: { from, to, insert: formatted },
|
||||
selection: formatsSelection
|
||||
? { anchor: from, head: from + formatted.length }
|
||||
: { anchor: from + formatted.length },
|
||||
});
|
||||
} catch (e: any) {
|
||||
emit("formatError", String(e?.message || e));
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!editorRef.value) return;
|
||||
|
||||
@@ -170,6 +199,13 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.formatRequestId,
|
||||
(val, oldVal) => {
|
||||
if (val && val !== oldVal) formatCurrentSql();
|
||||
}
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
view.value?.destroy();
|
||||
});
|
||||
|
||||
@@ -7,6 +7,8 @@ export default {
|
||||
newQuery: "New Query",
|
||||
execute: "Execute",
|
||||
executeShortcut: "Execute selection/query (Cmd+Enter)",
|
||||
formatSql: "Format SQL",
|
||||
formatSqlFailed: "Failed to format SQL",
|
||||
},
|
||||
sidebar: {
|
||||
connections: "CONNECTIONS",
|
||||
|
||||
@@ -7,6 +7,8 @@ export default {
|
||||
newQuery: "新建查询",
|
||||
execute: "执行",
|
||||
executeShortcut: "执行选中/全部 (Cmd+Enter)",
|
||||
formatSql: "格式化 SQL",
|
||||
formatSqlFailed: "SQL 格式化失败",
|
||||
},
|
||||
sidebar: {
|
||||
connections: "连接",
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
export type SqlFormatDialect = "mysql" | "postgres" | "sqlite" | "sqlserver" | "generic";
|
||||
|
||||
function formatterLanguage(dialect: SqlFormatDialect) {
|
||||
switch (dialect) {
|
||||
case "mysql":
|
||||
return "mysql";
|
||||
case "postgres":
|
||||
return "postgresql";
|
||||
case "sqlite":
|
||||
return "sqlite";
|
||||
case "sqlserver":
|
||||
return "transactsql";
|
||||
default:
|
||||
return "sql";
|
||||
}
|
||||
}
|
||||
|
||||
export async function formatSqlText(sql: string, dialect: SqlFormatDialect = "generic"): Promise<string> {
|
||||
if (!sql.trim()) return sql;
|
||||
const { format } = await import("sql-formatter");
|
||||
return format(sql, {
|
||||
language: formatterLanguage(dialect),
|
||||
keywordCase: "upper",
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user