From 718846516135280957eb96c73906c9d8a3979e51 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Sun, 17 May 2026 13:59:42 +0800 Subject: [PATCH] feat(app): open sql files in editor --- Cargo.lock | 356 ++++++++++++++++++++++++- src-tauri/Cargo.toml | 1 + src-tauri/src/commands/external_sql.rs | 94 +++++++ src-tauri/src/commands/mod.rs | 1 + src-tauri/src/lib.rs | 37 ++- src-tauri/tauri.conf.json | 9 + src/App.vue | 32 ++- src/composables/useTauriEvents.ts | 20 +- src/lib/api.ts | 1 + src/lib/http.ts | 4 + src/lib/sqlFileOpen.ts | 14 + src/lib/tauri.ts | 4 + tests/sqlFileOpen.test.ts | 18 ++ 13 files changed, 576 insertions(+), 15 deletions(-) create mode 100644 src-tauri/src/commands/external_sql.rs create mode 100644 src/lib/sqlFileOpen.ts create mode 100644 tests/sqlFileOpen.test.ts diff --git a/Cargo.lock b/Cargo.lock index 080cd955f0..27b9a76c44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -355,6 +355,30 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-compression" version = "0.4.42" @@ -367,6 +391,96 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.4", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -389,6 +503,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -673,6 +793,19 @@ dependencies = [ "objc2", ] +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "blowfish" version = "0.9.1" @@ -1585,6 +1718,7 @@ dependencies = [ "tauri-plugin-log", "tauri-plugin-process", "tauri-plugin-shell", + "tauri-plugin-single-instance", "tauri-plugin-updater", "tauri-plugin-window-state", "tiberius", @@ -1799,7 +1933,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2040,6 +2174,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + [[package]] name = "enum-as-inner" version = "0.6.1" @@ -2071,6 +2211,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ "enumflags2_derive", + "serde", ] [[package]] @@ -2118,7 +2259,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2143,6 +2284,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -2380,6 +2531,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.32" @@ -2826,6 +2990,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -3093,7 +3263,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.62.2", ] [[package]] @@ -4057,7 +4227,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4142,7 +4312,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4539,6 +4709,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "os_pipe" version = "1.2.3" @@ -4546,7 +4726,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.45.0", + "windows-sys 0.61.2", ] [[package]] @@ -4797,6 +4977,17 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs1" version = "0.7.5" @@ -4908,6 +5099,20 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -5810,7 +6015,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5902,7 +6107,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6525,7 +6730,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7307,6 +7512,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.18", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + [[package]] name = "tauri-plugin-updater" version = "2.10.1" @@ -7465,7 +7685,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7964,7 +8184,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -8005,6 +8225,17 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + [[package]] name = "unic-char-property" version = "0.9.0" @@ -8576,7 +8807,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -9357,6 +9588,67 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.2", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow 1.0.2", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.48" @@ -9543,3 +9835,43 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zvariant" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.2", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "winnow 1.0.2", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 84d65e611f..af141c28c1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -29,6 +29,7 @@ anyhow = "1" chrono = { version = "0.4", features = ["serde"] } tauri-plugin-dialog = "2.7.0" tauri-plugin-fs = "2.5.0" +tauri-plugin-single-instance = "2.4.2" tauri-plugin-shell = "2" tauri-plugin-updater = "2" tauri-plugin-process = "2" diff --git a/src-tauri/src/commands/external_sql.rs b/src-tauri/src/commands/external_sql.rs new file mode 100644 index 0000000000..bdccc5f0c4 --- /dev/null +++ b/src-tauri/src/commands/external_sql.rs @@ -0,0 +1,94 @@ +use std::path::{Path, PathBuf}; +use std::sync::Mutex; + +#[tauri::command] +pub fn pending_open_sql_files(state: tauri::State<'_, ExternalSqlOpenState>) -> Vec { + let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + let mut paths = sql_file_paths_from_args(std::env::args().skip(1), &cwd); + paths.extend(state.drain()); + dedupe_paths(paths) +} + +#[derive(Default)] +pub struct ExternalSqlOpenState { + pending: Mutex>, +} + +impl ExternalSqlOpenState { + pub fn push(&self, paths: Vec) { + if paths.is_empty() { + return; + } + if let Ok(mut pending) = self.pending.lock() { + pending.extend(paths); + } + } + + fn drain(&self) -> Vec { + self.pending.lock().map(|mut pending| pending.drain(..).collect()).unwrap_or_default() + } +} + +pub fn sql_file_paths_from_args(args: I, cwd: &Path) -> Vec +where + I: IntoIterator, + S: AsRef, +{ + args.into_iter().filter_map(|arg| sql_file_path_from_arg(arg.as_ref(), cwd)).collect() +} + +fn sql_file_path_from_arg(arg: &str, cwd: &Path) -> Option { + if arg.starts_with('-') { + return None; + } + + let path = PathBuf::from(arg); + if !is_sql_file_path(&path) { + return None; + } + + let resolved = if path.is_absolute() { path } else { cwd.join(path) }; + Some(resolved.to_string_lossy().to_string()) +} + +pub fn is_sql_file_path(path: &Path) -> bool { + path.extension().and_then(|ext| ext.to_str()).map(|ext| ext.eq_ignore_ascii_case("sql")).unwrap_or(false) +} + +fn dedupe_paths(paths: Vec) -> Vec { + let mut unique = Vec::new(); + for path in paths { + if !unique.contains(&path) { + unique.push(path); + } + } + unique +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn filters_sql_file_args_case_insensitively() { + let paths = sql_file_paths_from_args(["/tmp/a.sql", "--flag", "/tmp/b.SQL", "/tmp/c.txt"], Path::new("/work")); + + assert_eq!(paths, vec!["/tmp/a.sql", "/tmp/b.SQL"]); + } + + #[test] + fn resolves_relative_sql_file_args_against_cwd() { + let paths = sql_file_paths_from_args(["queries/report.sql"], Path::new("/work")); + + assert_eq!(paths, vec!["/work/queries/report.sql"]); + } + + #[test] + fn drains_pending_sql_file_paths_once() { + let state = ExternalSqlOpenState::default(); + state.push(vec!["/tmp/a.sql".to_string()]); + + assert_eq!(state.drain(), vec!["/tmp/a.sql"]); + assert!(state.drain().is_empty()); + } +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 3e3841a3c6..2e4953d201 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -4,6 +4,7 @@ pub mod connection; #[allow(dead_code, unused_imports)] mod connection_secrets; pub mod database_export; +pub mod external_sql; pub mod history; pub mod mcp_bridge; pub mod mongo_cmd; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b58440dacf..e6c362337e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -7,7 +7,7 @@ use commands::connection::AppState; use dbx_core::storage::Storage; use std::sync::Arc; use std::time::Instant; -use tauri::{Manager, RunEvent}; +use tauri::{Emitter, Manager, RunEvent}; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { @@ -18,6 +18,19 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_single_instance::init(|app, args, cwd| { + let paths = commands::external_sql::sql_file_paths_from_args(args, std::path::Path::new(&cwd)); + if !paths.is_empty() { + if let Some(state) = app.try_state::() { + state.push(paths.clone()); + } + let _ = app.emit("dbx-open-sql-files", paths); + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + } + } + })) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_process::init()) @@ -49,6 +62,7 @@ pub fn run() { let state = Arc::new(AppState::new_with_plugin_dir(storage, data_dir.join("plugins"))); app.manage(state.clone()); + app.manage(commands::external_sql::ExternalSqlOpenState::default()); let app_handle = app.handle().clone(); commands::mcp_bridge::start(app_handle, state); @@ -132,6 +146,7 @@ pub fn run() { commands::sql_file::preview_sql_file, commands::sql_file::execute_sql_file, commands::sql_file::cancel_sql_file_execution, + commands::external_sql::pending_open_sql_files, commands::table_import::preview_table_import_file, commands::table_import::import_table_file, commands::table_import::cancel_table_import, @@ -189,6 +204,26 @@ pub fn run() { .build(tauri::generate_context!()) .expect("error while building tauri application") .run(|app_handle, event| { + #[cfg(target_os = "macos")] + if let RunEvent::Opened { urls } = &event { + let paths: Vec = urls + .iter() + .filter_map(|url| url.to_file_path().ok()) + .filter(|path| commands::external_sql::is_sql_file_path(path)) + .map(|path| path.to_string_lossy().to_string()) + .collect(); + if !paths.is_empty() { + if let Some(state) = app_handle.try_state::() { + state.push(paths.clone()); + } + let _ = app_handle.emit("dbx-open-sql-files", paths); + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + } + } + } + #[cfg(target_os = "macos")] if let RunEvent::Reopen { has_visible_windows, .. } = &event { if !has_visible_windows { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 895fb5edd3..9ece27b1d9 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -36,6 +36,15 @@ "active": true, "targets": "all", "createUpdaterArtifacts": true, + "fileAssociations": [ + { + "ext": ["sql"], + "name": "SQL Script", + "description": "SQL script", + "role": "Editor", + "mimeType": "application/sql" + } + ], "macOS": {}, "icon": [ "icons/32x32.png", diff --git a/src/App.vue b/src/App.vue index 39d4716504..2fe103af1f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -36,6 +36,7 @@ import { resolveDefaultDatabase } from "@/lib/defaultDatabase"; import { buildExecutableObjectSourceSql, objectSourceSaveExecutionMode } from "@/lib/objectSourceEditor"; import { resolveExecutableSql } from "@/lib/sqlExecutionTarget"; import { isTauriRuntime } from "@/lib/tauriRuntime"; +import { sqlFileTitleFromPath } from "@/lib/sqlFileOpen"; import { isCloseTabShortcut, isExecuteSqlShortcut, @@ -152,7 +153,7 @@ const { getDatabaseOptions } = useDatabaseOptions(); const { openLineageTarget, openDatabaseSearchTarget, onStructureEditorSaved, openTableTarget } = useNavigationTargets(dialogs); const { onExecuteSql, onReloadData, onPaginate, onSort } = useDataGridActions(activeTab); -const { setupTauriListeners, cleanupTauriListeners } = useTauriEvents({ openTableTarget }); +const { setupTauriListeners, cleanupTauriListeners } = useTauriEvents({ openTableTarget, openSqlFilePath }); const appVersion = ref(""); const isClassicLayout = computed(() => settingsStore.editorSettings.appLayout === "classic"); @@ -341,6 +342,34 @@ async function openSqlFile() { } } +async function openSqlFilePath(path: string) { + if (!isTauriRuntime()) return; + try { + const { readTextFile } = await import("@tauri-apps/plugin-fs"); + const content = await readTextFile(path); + const connectionId = + connectionStore.activeConnectionId || activeTab.value?.connectionId || connectionStore.connections[0]?.id || ""; + const connection = connectionId ? connectionStore.getConfig(connectionId) : undefined; + const database = activeTab.value?.database || (connection ? resolveDefaultDatabase(connection, []) : ""); + const tabId = queryStore.createTab(connectionId, database, sqlFileTitleFromPath(path), "query"); + queryStore.updateSql(tabId, content); + } catch (e: any) { + toast(t("toolbar.sqlOpenFailed", { message: e?.message || String(e) }), 5000); + } +} + +async function openPendingSqlFiles() { + if (!isTauriRuntime()) return; + try { + const paths = await api.pendingOpenSqlFiles(); + for (const path of paths) { + await openSqlFilePath(path); + } + } catch { + /* ignore startup file-open probing errors */ + } +} + async function newQuery() { const connId = connectionStore.activeConnectionId || connectionStore.connections[0]?.id; if (!connId) return; @@ -614,6 +643,7 @@ onMounted(async () => { }) .catch(() => {}); setupTauriListeners(); + void openPendingSqlFiles(); console.log(`[STARTUP] onMounted sync done: ${(performance.now() - mountStart).toFixed(0)}ms`); }); diff --git a/src/composables/useTauriEvents.ts b/src/composables/useTauriEvents.ts index eea207bb84..b8639460de 100644 --- a/src/composables/useTauriEvents.ts +++ b/src/composables/useTauriEvents.ts @@ -2,7 +2,10 @@ import { useConnectionStore } from "@/stores/connectionStore"; import { useQueryStore } from "@/stores/queryStore"; import type { NavigationTarget } from "@/composables/useNavigationTargets"; -export function useTauriEvents(deps: { openTableTarget: (target: NavigationTarget) => Promise }) { +export function useTauriEvents(deps: { + openTableTarget: (target: NavigationTarget) => Promise; + openSqlFilePath: (path: string) => Promise; +}) { const connectionStore = useConnectionStore(); const queryStore = useQueryStore(); const unlistenHandles: Array<() => void> = []; @@ -66,6 +69,21 @@ export function useTauriEvents(deps: { openTableTarget: (target: NavigationTarge console.error("[DBX] mcp-execute-query error:", e); } }).then((unlisten) => unlistenHandles.push(unlisten)); + + listen("dbx-open-sql-files", async (event) => { + try { + for (const path of event.payload) { + await deps.openSqlFilePath(path); + } + import("@tauri-apps/api/window").then(({ getCurrentWindow }) => + getCurrentWindow() + .setFocus() + .catch(() => {}), + ); + } catch (e) { + console.error("[DBX] dbx-open-sql-files error:", e); + } + }).then((unlisten) => unlistenHandles.push(unlisten)); }) .catch(() => {}); } diff --git a/src/lib/api.ts b/src/lib/api.ts index 917f5c151c..9c5fe411dc 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -89,6 +89,7 @@ export const previewSqlFile = forward("previewSqlFile"); export const executeSqlFile = forward("executeSqlFile"); export const cancelSqlFileExecution = forward("cancelSqlFileExecution"); export const listenSqlFileProgress = forward("listenSqlFileProgress"); +export const pendingOpenSqlFiles = forward("pendingOpenSqlFiles"); // Data Transfer export const startTransfer = forward("startTransfer"); diff --git a/src/lib/http.ts b/src/lib/http.ts index 8b845ba326..da2f0224b6 100644 --- a/src/lib/http.ts +++ b/src/lib/http.ts @@ -416,6 +416,10 @@ export async function listenSqlFileProgress(_handler: (progress: SqlFileProgress return () => {}; } +export async function pendingOpenSqlFiles(): Promise { + return []; +} + // --------------------------------------------------------------------------- // Data Transfer // --------------------------------------------------------------------------- diff --git a/src/lib/sqlFileOpen.ts b/src/lib/sqlFileOpen.ts new file mode 100644 index 0000000000..c5508d69f7 --- /dev/null +++ b/src/lib/sqlFileOpen.ts @@ -0,0 +1,14 @@ +export function isSqlFilePath(path: string): boolean { + return /\.sql$/i.test(path.trim()); +} + +export function sqlFileTitleFromPath(path: string): string { + const trimmed = path.trim(); + const normalized = trimmed.replace(/\\/g, "/"); + const name = normalized.split("/").filter(Boolean).pop(); + return name || "script.sql"; +} + +export function externalSqlFilePaths(paths: string[]): string[] { + return paths.filter(isSqlFilePath); +} diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index 086320a8a7..1f472bfb55 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -80,6 +80,10 @@ export async function loadAiConfig(): Promise { return invoke("load_ai_config"); } +export async function pendingOpenSqlFiles(): Promise { + return invoke("pending_open_sql_files"); +} + // --- AI Conversations --- export interface AiChatMessage { diff --git a/tests/sqlFileOpen.test.ts b/tests/sqlFileOpen.test.ts new file mode 100644 index 0000000000..7473021445 --- /dev/null +++ b/tests/sqlFileOpen.test.ts @@ -0,0 +1,18 @@ +import { strict as assert } from "node:assert"; +import test from "node:test"; +import { externalSqlFilePaths, isSqlFilePath, sqlFileTitleFromPath } from "../src/lib/sqlFileOpen.ts"; + +test("detects SQL file paths case-insensitively", () => { + assert.equal(isSqlFilePath("/tmp/report.sql"), true); + assert.equal(isSqlFilePath("C:\\Users\\me\\Query.SQL"), true); + assert.equal(isSqlFilePath("/tmp/report.txt"), false); +}); + +test("derives a tab title from Unix or Windows SQL paths", () => { + assert.equal(sqlFileTitleFromPath("/tmp/report.sql"), "report.sql"); + assert.equal(sqlFileTitleFromPath("C:\\Users\\me\\Query.SQL"), "Query.SQL"); +}); + +test("filters external open payloads down to SQL files", () => { + assert.deepEqual(externalSqlFilePaths(["/tmp/a.sql", "/tmp/readme.md", "/tmp/b.SQL"]), ["/tmp/a.sql", "/tmp/b.SQL"]); +});