Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions src-tauri/src/dap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,93 @@ pub fn dap_stop(state: State<'_, DapState>, session: String) -> Result<(), Strin
Ok(())
}

/// 为调试构建编译型语言,返回可执行文件路径。
/// Rust: cargo build → target/debug/<package>;C/C++: 用 cc/c++ -g 编译到临时文件。
#[tauri::command]
pub async fn dap_build(
language: String,
file_path: String,
root: String,
) -> Result<String, String> {
tokio::task::spawn_blocking(move || match language.as_str() {
"rust" => build_rust(&root),
"c" => build_cc("cc", &file_path),
"cpp" => build_cc("c++", &file_path),
_ => Err(format!("{} 暂不支持编译调试", language)),
})
.await
.map_err(|e| format!("构建任务失败: {}", e))?
}

fn build_rust(root: &str) -> Result<String, String> {
let exe =
find_in_path("cargo").ok_or_else(|| "未找到 cargo(请安装 Rust 工具链)".to_string())?;
let out = Command::new(&exe)
.args(["build"])
.current_dir(root)
.env("PATH", augmented_path())
.output()
.map_err(|e| format!("执行 cargo 失败: {}", e))?;
if !out.status.success() {
return Err(format!(
"cargo build 失败:\n{}",
String::from_utf8_lossy(&out.stderr)
));
}
// 解析 Cargo.toml 的 package name
let toml = std::fs::read_to_string(std::path::Path::new(root).join("Cargo.toml"))
.map_err(|e| format!("读取 Cargo.toml 失败: {}", e))?;
let mut name = String::new();
for line in toml.lines() {
let l = line.trim();
if let Some(rest) = l.strip_prefix("name") {
if let Some(v) = rest.trim_start_matches(['=', ' ']).strip_prefix('"') {
if let Some(end) = v.find('"') {
name = v[..end].to_string();
break;
}
}
}
}
if name.is_empty() {
return Err("无法从 Cargo.toml 解析 package name".to_string());
}
let mut bin = std::path::Path::new(root).join("target/debug").join(&name);
if cfg!(windows) {
bin.set_extension("exe");
}
if !bin.is_file() {
return Err(format!("未找到可执行文件: {}", bin.display()));
}
Ok(bin.to_string_lossy().to_string())
}

fn build_cc(compiler: &str, file_path: &str) -> Result<String, String> {
let exe =
find_in_path(compiler).ok_or_else(|| format!("未找到编译器 {}(请安装)", compiler))?;
let stamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let mut out_path = std::env::temp_dir().join(format!("codeforge-dbg-{}", stamp));
if cfg!(windows) {
out_path.set_extension("exe");
}
let out_str = out_path.to_string_lossy().to_string();
let result = Command::new(&exe)
.args(["-g", "-O0", "-o", &out_str, file_path])
.env("PATH", augmented_path())
.output()
.map_err(|e| format!("执行 {} 失败: {}", compiler, e))?;
if !result.status.success() {
return Err(format!(
"编译失败:\n{}",
String::from_utf8_lossy(&result.stderr)
));
}
Ok(out_str)
}

// 可安装的调试适配器:(id, 展示名, 安装命令)
fn adapter_defs() -> Vec<(&'static str, &'static str, &'static str)> {
vec![
Expand All @@ -215,6 +302,7 @@ fn adapter_defs() -> Vec<(&'static str, &'static str, &'static str)> {
"Go (delve)",
"go install github.com/go-delve/delve/cmd/dlv@latest",
),
("lldb-dap", "Rust / C / C++ (lldb-dap)", "brew install llvm"),
]
}

Expand All @@ -223,6 +311,12 @@ fn adapter_installed(id: &str) -> bool {
// debugpy 校验模块可导入
"debugpy" => dap_available("python".to_string()),
"delve" => find_in_path("dlv").is_some(),
// lldb-dap 随 LLVM 提供,brew 的 llvm 是 keg-only 不在 PATH,额外探测常见路径
"lldb-dap" => {
find_in_path("lldb-dap").is_some()
|| std::path::Path::new("/opt/homebrew/opt/llvm/bin/lldb-dap").is_file()
|| std::path::Path::new("/usr/local/opt/llvm/bin/lldb-dap").is_file()
}
_ => false,
}
}
Expand Down
148 changes: 88 additions & 60 deletions src-tauri/src/db/duckdb.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,84 @@
use super::{DataSource, DbExecutor, SqlResultSet, SqlRunResult, split_sql};
use super::{DataSource, DbExecutor, SqlResultSet, SqlRunResult, TxnConn, split_sql};
use duckdb::Connection;
use serde_json::Value as JsonValue;

pub(crate) struct DuckdbExecutor;

fn open_conn(source: &DataSource) -> Result<Connection, String> {
let conn = match source.file.as_deref() {
Some(p) if !p.trim().is_empty() => Connection::open(p),
_ => Connection::open_in_memory(),
};
conn.map_err(|e| format!("打开 DuckDB 失败: {}", e))
}

/// 在已有连接上执行脚本(run 与事务会话共用)
fn run_on_conn(conn: &Connection, sql: &str) -> SqlRunResult {
let mut result = SqlRunResult::new();
'stmts: for stmt_sql in split_sql(sql) {
let mut stmt = match conn.prepare(&stmt_sql) {
Ok(s) => s,
Err(e) => {
result.error = Some(e.to_string());
break 'stmts;
}
};
let ncol = stmt.column_count();
if ncol > 0 {
let columns: Vec<String> = stmt.column_names().iter().map(|s| s.to_string()).collect();
let rows_iter = stmt.query_map([], |row| {
let mut v = Vec::with_capacity(ncol);
for idx in 0..ncol {
v.push(value_to_json(row.get(idx)?));
}
Ok(v)
});
match rows_iter {
Ok(it) => {
let mut rows = Vec::new();
for r in it {
match r {
Ok(rw) => rows.push(rw),
Err(e) => {
result.error = Some(e.to_string());
break 'stmts;
}
}
}
result.result_sets.push(SqlResultSet { columns, rows });
}
Err(e) => {
result.error = Some(e.to_string());
break 'stmts;
}
}
} else {
match stmt.execute([]) {
Ok(affected) => result.messages.push(format!("OK,影响 {} 行", affected)),
Err(e) => {
result.error = Some(e.to_string());
break 'stmts;
}
}
}
}
result
}

struct DuckdbTxn {
conn: Connection,
}
impl TxnConn for DuckdbTxn {
fn exec(&mut self, sql: &str) -> SqlRunResult {
run_on_conn(&self.conn, sql)
}
fn finish(&mut self, commit: bool) -> Result<(), String> {
self.conn
.execute_batch(if commit { "COMMIT" } else { "ROLLBACK" })
.map_err(|e| e.to_string())
}
}

fn value_to_json(v: duckdb::types::Value) -> JsonValue {
use duckdb::types::Value::*;
match v {
Expand Down Expand Up @@ -33,67 +108,20 @@ impl DbExecutor for DuckdbExecutor {
}

fn run(&self, sql: &str, source: &DataSource) -> SqlRunResult {
let mut result = SqlRunResult::new();
let conn = match source.file.as_deref() {
Some(p) if !p.trim().is_empty() => Connection::open(p),
_ => Connection::open_in_memory(),
};
let conn = match conn {
Ok(c) => c,
match open_conn(source) {
Ok(conn) => run_on_conn(&conn, sql),
Err(e) => {
result.error = Some(format!("打开 DuckDB 失败: {}", e));
return result;
}
};

'stmts: for stmt_sql in split_sql(sql) {
let mut stmt = match conn.prepare(&stmt_sql) {
Ok(s) => s,
Err(e) => {
result.error = Some(e.to_string());
break 'stmts;
}
};
let ncol = stmt.column_count();
if ncol > 0 {
let columns: Vec<String> =
stmt.column_names().iter().map(|s| s.to_string()).collect();
let rows_iter = stmt.query_map([], |row| {
let mut v = Vec::with_capacity(ncol);
for idx in 0..ncol {
v.push(value_to_json(row.get(idx)?));
}
Ok(v)
});
match rows_iter {
Ok(it) => {
let mut rows = Vec::new();
for r in it {
match r {
Ok(rw) => rows.push(rw),
Err(e) => {
result.error = Some(e.to_string());
break 'stmts;
}
}
}
result.result_sets.push(SqlResultSet { columns, rows });
}
Err(e) => {
result.error = Some(e.to_string());
break 'stmts;
}
}
} else {
match stmt.execute([]) {
Ok(affected) => result.messages.push(format!("OK,影响 {} 行", affected)),
Err(e) => {
result.error = Some(e.to_string());
break 'stmts;
}
}
let mut result = SqlRunResult::new();
result.error = Some(e);
result
}
}
result
}

fn begin(&self, source: &DataSource) -> Result<Box<dyn TxnConn>, String> {
let conn = open_conn(source)?;
conn.execute_batch("BEGIN TRANSACTION")
.map_err(|e| format!("开启事务失败: {}", e))?;
Ok(Box::new(DuckdbTxn { conn }))
}
}
91 changes: 91 additions & 0 deletions src-tauri/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,103 @@ pub(crate) fn resolve_endpoint(source: &DataSource, default_port: u16) -> Result
}
}

/// 交互式事务会话:持有一条连接,跨多次调用在同一连接上执行。
pub(crate) trait TxnConn: Send {
/// 在持有连接上执行 SQL(错误写入 result.error)
fn exec(&mut self, sql: &str) -> SqlRunResult;
/// 结束事务:commit=true 提交,否则回滚
fn finish(&mut self, commit: bool) -> Result<(), String>;
}

/// 数据库执行器接口:新增数据库类型只需实现本 trait 并在 executors() 注册。
pub(crate) trait DbExecutor: Send + Sync {
/// 是否处理该数据源类型(如 sqlite 同时处理 "sqlite" 与 "memory")
fn handles(&self, kind: &str) -> bool;
/// 执行脚本,返回结构化结果(错误写入 result.error,不以 Err 形式返回)
fn run(&self, sql: &str, source: &DataSource) -> SqlRunResult;
/// 开启交互式事务,返回持有连接的会话;不支持事务的执行器返回 Err。
fn begin(&self, _source: &DataSource) -> Result<Box<dyn TxnConn>, String> {
Err("该数据源暂不支持交互式事务".to_string())
}
}

/// 全局事务状态:同一时刻仅允许一个进行中的交互式事务(一个编辑器一个事务)。
pub struct TxnState(pub std::sync::Arc<std::sync::Mutex<Option<Box<dyn TxnConn>>>>);

impl TxnState {
pub fn new() -> Self {
Self(std::sync::Arc::new(std::sync::Mutex::new(None)))
}
}

impl Default for TxnState {
fn default() -> Self {
Self::new()
}
}

/// 开启事务(已有进行中的事务会先回滚丢弃)。
#[tauri::command]
pub async fn tx_begin(source: DataSource, state: tauri::State<'_, TxnState>) -> Result<(), String> {
let slot = state.0.clone();
tokio::task::spawn_blocking(move || {
let execs = executors();
let exec = execs
.iter()
.find(|e| e.handles(&source.kind))
.ok_or_else(|| format!("不支持的数据源类型: {}", source.kind))?;
let conn = exec.begin(&source)?;
let mut guard = slot.lock().map_err(|e| e.to_string())?;
if let Some(mut old) = guard.take() {
let _ = old.finish(false);
}
*guard = Some(conn);
Ok(())
})
.await
.map_err(|e| format!("事务任务失败: {}", e))?
}

/// 在进行中的事务上执行 SQL。
#[tauri::command]
pub async fn tx_exec(
sql: String,
state: tauri::State<'_, TxnState>,
) -> Result<SqlRunResult, String> {
let slot = state.0.clone();
tokio::task::spawn_blocking(move || {
let mut guard = slot.lock().map_err(|e| e.to_string())?;
let conn = guard
.as_mut()
.ok_or_else(|| "没有进行中的事务".to_string())?;
Ok(conn.exec(&sql))
})
.await
.map_err(|e| format!("事务任务失败: {}", e))?
}

/// 结束事务:commit=true 提交,否则回滚。
#[tauri::command]
pub async fn tx_finish(commit: bool, state: tauri::State<'_, TxnState>) -> Result<(), String> {
let slot = state.0.clone();
tokio::task::spawn_blocking(move || {
let mut guard = slot.lock().map_err(|e| e.to_string())?;
match guard.take() {
Some(mut conn) => conn.finish(commit),
None => Err("没有进行中的事务".to_string()),
}
})
.await
.map_err(|e| format!("事务任务失败: {}", e))?
}

/// 是否有进行中的事务。
#[tauri::command]
pub async fn tx_active(state: tauri::State<'_, TxnState>) -> Result<bool, String> {
let slot = state.0.clone();
tokio::task::spawn_blocking(move || Ok(slot.lock().map(|g| g.is_some()).unwrap_or(false)))
.await
.map_err(|e| format!("事务任务失败: {}", e))?
}

/// 已注册的执行器。新增数据库类型:在此加一行。
Expand Down
Loading
Loading