From f2de6942f62e1a538bd6f9fe9e43778ef2fb3e04 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Wed, 3 Jun 2026 19:30:58 +0800 Subject: [PATCH 01/29] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E5=8E=86=E5=8F=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/Cargo.lock | 76 +++++++- src-tauri/Cargo.toml | 1 + src-tauri/src/execution.rs | 190 +++++++++++++++++-- src-tauri/src/main.rs | 6 +- src-tauri/src/plugins/mod.rs | 1 + src/App.vue | 18 +- src/components/AppHeader.vue | 4 +- src/components/ExecutionHistory.vue | 284 ++++++++++++++++++++++++++++ src/types/app.ts | 1 + 9 files changed, 560 insertions(+), 21 deletions(-) create mode 100644 src/components/ExecutionHistory.vue diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7630d5b..6fe8bda 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -17,6 +17,7 @@ dependencies = [ "regex", "reqwest 0.11.27", "rfd 0.15.4", + "rusqlite", "serde", "serde_json", "tar", @@ -60,6 +61,18 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -948,7 +961,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1181,6 +1194,18 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.3.0" @@ -1708,12 +1733,30 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "heck" version = "0.4.1" @@ -2310,6 +2353,17 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -2430,7 +2484,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.12", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3432,6 +3486,20 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags 2.9.1", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -4462,7 +4530,7 @@ dependencies = [ "serde_with", "swift-rs", "thiserror 2.0.12", - "toml 0.9.5", + "toml 1.1.2+spec-1.1.0", "url", "urlpattern", "uuid", @@ -4837,7 +4905,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.12", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7f383fb..38999ab 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -29,6 +29,7 @@ log = "0.4" fern = "0.7.1" dirs = "6.0.0" regex = "1.11.1" +rusqlite = { version = "0.32", features = ["bundled"] } reqwest = { version = "0.11", features = ["json", "stream"] } futures-util = "0.3" rfd = "0.15" diff --git a/src-tauri/src/execution.rs b/src-tauri/src/execution.rs index 279a157..7751609 100644 --- a/src-tauri/src/execution.rs +++ b/src-tauri/src/execution.rs @@ -1,11 +1,13 @@ use crate::plugins::{CodeExecutionRequest, ExecutionResult, PluginManager}; use log::{error, info, warn}; +use rusqlite::{Connection, params}; +use serde::Serialize; use std::collections::HashMap; use std::fs; use std::io::{BufRead, BufReader}; use std::path::PathBuf; use std::process::{Command, Stdio}; -use std::sync::{Arc, OnceLock, mpsc}; +use std::sync::{Arc, Mutex as StdMutex, OnceLock, mpsc}; use std::thread; use std::time::{SystemTime, UNIX_EPOCH}; use tauri::{AppHandle, Emitter, State}; @@ -21,9 +23,154 @@ pub struct ExecutionTask { pub stop_flag: Arc>, } -pub type ExecutionHistory = Mutex>; pub type PluginManagerState = Mutex; +#[derive(Debug, Serialize)] +pub struct ExecutionHistoryPage { + pub items: Vec, + pub total: u64, +} + +pub struct ExecutionHistory { + conn: StdMutex, +} + +impl ExecutionHistory { + pub fn new() -> Result { + let db_path = get_codeforge_db_path()?; + let conn = + Connection::open(&db_path).map_err(|e| format!("打开执行历史数据库失败: {}", e))?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS execution_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + success INTEGER NOT NULL, + code TEXT NOT NULL, + stdout TEXT NOT NULL, + stderr TEXT NOT NULL, + execution_time INTEGER NOT NULL, + timestamp INTEGER NOT NULL, + language TEXT NOT NULL + )", + [], + ) + .map_err(|e| format!("初始化执行历史数据库失败: {}", e))?; + + Ok(Self { + conn: StdMutex::new(conn), + }) + } + + fn insert(&self, result: &ExecutionResult) -> Result<(), String> { + let conn = self + .conn + .lock() + .map_err(|_| "执行历史数据库锁错误".to_string())?; + + conn.execute( + "INSERT INTO execution_history + (success, code, stdout, stderr, execution_time, timestamp, language) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + params![ + if result.success { 1 } else { 0 }, + &result.code, + &result.stdout, + &result.stderr, + result.execution_time as i64, + result.timestamp as i64, + &result.language, + ], + ) + .map_err(|e| format!("保存执行历史失败: {}", e))?; + + Ok(()) + } + + fn list(&self) -> Result, String> { + let conn = self + .conn + .lock() + .map_err(|_| "执行历史数据库锁错误".to_string())?; + let mut statement = conn + .prepare( + "SELECT success, code, stdout, stderr, execution_time, timestamp, language + FROM execution_history + ORDER BY id ASC", + ) + .map_err(|e| format!("读取执行历史失败: {}", e))?; + + let rows = statement + .query_map([], |row| { + Ok(ExecutionResult { + success: row.get::<_, i64>(0)? != 0, + code: row.get(1)?, + stdout: row.get(2)?, + stderr: row.get(3)?, + execution_time: row.get::<_, i64>(4)? as u128, + timestamp: row.get::<_, i64>(5)? as u64, + language: row.get(6)?, + }) + }) + .map_err(|e| format!("读取执行历史失败: {}", e))?; + + rows.collect::, _>>() + .map_err(|e| format!("读取执行历史失败: {}", e)) + } + + fn list_page(&self, offset: u64, limit: u64) -> Result { + let limit = limit.clamp(1, 100); + let conn = self + .conn + .lock() + .map_err(|_| "执行历史数据库锁错误".to_string())?; + + let total = conn + .query_row("SELECT COUNT(*) FROM execution_history", [], |row| { + row.get::<_, i64>(0) + }) + .map_err(|e| format!("统计执行历史失败: {}", e))? as u64; + + let mut statement = conn + .prepare( + "SELECT success, code, stdout, stderr, execution_time, timestamp, language + FROM execution_history + ORDER BY id DESC + LIMIT ?1 OFFSET ?2", + ) + .map_err(|e| format!("读取执行历史失败: {}", e))?; + + let rows = statement + .query_map(params![limit as i64, offset as i64], |row| { + Ok(ExecutionResult { + success: row.get::<_, i64>(0)? != 0, + code: row.get(1)?, + stdout: row.get(2)?, + stderr: row.get(3)?, + execution_time: row.get::<_, i64>(4)? as u128, + timestamp: row.get::<_, i64>(5)? as u64, + language: row.get(6)?, + }) + }) + .map_err(|e| format!("读取执行历史失败: {}", e))?; + + let items = rows + .collect::, _>>() + .map_err(|e| format!("读取执行历史失败: {}", e))?; + + Ok(ExecutionHistoryPage { items, total }) + } + + fn clear(&self) -> Result<(), String> { + let conn = self + .conn + .lock() + .map_err(|_| "执行历史数据库锁错误".to_string())?; + conn.execute("DELETE FROM execution_history", []) + .map_err(|e| format!("清空执行历史失败: {}", e))?; + Ok(()) + } +} + // 全局任务管理器 type TaskManager = Arc>>; static TASK_MANAGER: OnceLock = OnceLock::new(); @@ -50,6 +197,20 @@ fn get_codeforge_cache_dir(language: &str) -> Result { Ok(cache_dir) } +fn get_codeforge_db_path() -> Result { + let home_dir = dirs::home_dir().ok_or("无法获取用户主目录")?; + let codeforge_dir = home_dir.join(".codeforge"); + fs::create_dir_all(&codeforge_dir).map_err(|e| format!("创建配置目录失败: {}", e))?; + let db_path = codeforge_dir.join("codeforge.sqlite"); + let old_db_path = codeforge_dir.join("execution_history.sqlite"); + + if !db_path.exists() && old_db_path.exists() { + fs::rename(&old_db_path, &db_path).map_err(|e| format!("迁移执行历史数据库失败: {}", e))?; + } + + Ok(db_path) +} + // 检查是否应该过滤 stderr 行 fn should_filter_stderr_line(language: &str, line: &str) -> bool { match language { @@ -377,6 +538,7 @@ pub async fn execute_code( let mut result = ExecutionResult { success: status.success(), + code: request.code.clone(), stdout: stdout_lines.join("\n"), stderr: stderr_lines.join("\n"), execution_time, @@ -410,12 +572,7 @@ pub async fn execute_code( ); drop(manager); - let mut history_guard = history.lock().await; - history_guard.push(result.clone()); - - if history_guard.len() > 100 { - history_guard.remove(0); - } + history.insert(&result)?; info!("执行代码 -> 调用插件 [ {} ] 完成", request.language); return Ok(result); @@ -453,14 +610,21 @@ pub async fn execute_code( pub async fn get_execution_history( history: State<'_, ExecutionHistory>, ) -> Result, String> { - let history_guard = history.lock().await; - Ok(history_guard.clone()) + history.list() +} + +// 分页获取执行历史 +#[tauri::command] +pub async fn get_execution_history_page( + offset: u64, + limit: u64, + history: State<'_, ExecutionHistory>, +) -> Result { + history.list_page(offset, limit) } // 清空执行历史 #[tauri::command] pub async fn clear_execution_history(history: State<'_, ExecutionHistory>) -> Result<(), String> { - let mut history_guard = history.lock().await; - history_guard.clear(); - Ok(()) + history.clear() } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index bae0444..44bc17b 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -36,7 +36,8 @@ use crate::env_providers::{ }; use crate::execution::{ ExecutionHistory, PluginManagerState as ExecutionPluginManagerState, clear_execution_history, - execute_code, get_execution_history, is_execution_running, stop_execution, + execute_code, get_execution_history, get_execution_history_page, is_execution_running, + stop_execution, }; use crate::filesystem::{ get_text_file_meta, read_directory_tree, read_file_lines, read_file_text, write_file_text, @@ -71,7 +72,7 @@ fn main() { .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_fs::init()) - .manage(ExecutionHistory::default()) + .manage(ExecutionHistory::new().expect("failed to initialize execution history database")) .manage(ExecutionPluginManagerState::new(PluginManager::new())) .manage(EnvironmentManagerState::new(env_manager)) .setup(|app| { @@ -116,6 +117,7 @@ fn main() { stop_execution, is_execution_running, get_execution_history, + get_execution_history_page, clear_execution_history, // 信息相关命令 get_info, diff --git a/src-tauri/src/plugins/mod.rs b/src-tauri/src/plugins/mod.rs index deff308..da2e1d9 100644 --- a/src-tauri/src/plugins/mod.rs +++ b/src-tauri/src/plugins/mod.rs @@ -8,6 +8,7 @@ use std::path::PathBuf; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ExecutionResult { pub success: bool, + pub code: String, pub stdout: String, pub stderr: String, pub execution_time: u128, diff --git a/src/App.vue b/src/App.vue index d9089b2..b8996fc 100644 --- a/src/App.vue +++ b/src/App.vue @@ -13,6 +13,7 @@ @layout-change="handleLayoutChange" @open-file="handleOpenFileClick" @save-file="saveFile" + @show-history="showHistory = true" @show-settings="showSettings = true" @load-example="loadExample"> @@ -139,6 +140,11 @@ + + + @@ -147,7 +153,7 @@ diff --git a/src/types/app.ts b/src/types/app.ts index 2bf6ed2..b732cb2 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -1,6 +1,7 @@ export interface ExecutionResult { success: boolean + code: string stdout: string stderr: string execution_time: number From 638613ef3c79c14ceef4185c3660e7c716d380da Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Wed, 3 Jun 2026 19:41:44 +0800 Subject: [PATCH 02/29] =?UTF-8?q?feat:=20=E6=89=A7=E8=A1=8C=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=20task=5Fid=20=E8=B7=AF=E7=94=B1=E5=B9=B6=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=B0=B1=E5=9C=B0=E8=BF=90=E8=A1=8C=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/execution.rs | 109 +++++++++++++++++----------- src-tauri/src/plugins/mod.rs | 4 + src/App.vue | 25 +++++-- src/composables/useCodeExecution.ts | 51 +++++++++---- src/composables/useEventManager.ts | 36 +++------ 5 files changed, 137 insertions(+), 88 deletions(-) diff --git a/src-tauri/src/execution.rs b/src-tauri/src/execution.rs index 7751609..ba59e0d 100644 --- a/src-tauri/src/execution.rs +++ b/src-tauri/src/execution.rs @@ -229,32 +229,32 @@ fn should_filter_stderr_line(language: &str, line: &str) -> bool { } } -// 停止执行命令 +// 停止执行命令(按 task_id 停止指定的运行任务) #[tauri::command] -pub async fn stop_execution(language: String) -> Result { +pub async fn stop_execution(task_id: String) -> Result { let task_manager = init_task_manager(); let mut guard = task_manager.lock().await; - if let Some(task) = guard.remove(&language) { + if let Some(task) = guard.remove(&task_id) { // 设置停止标志 { let mut stop_flag = task.stop_flag.lock().await; *stop_flag = true; } - info!("停止执行 -> 成功设置停止标志给语言 [ {} ]", language); + info!("停止执行 -> 成功设置停止标志给任务 [ {} ]", task_id); Ok(true) } else { - warn!("停止执行 -> 语言 [ {} ] 没有正在运行的任务", language); + warn!("停止执行 -> 任务 [ {} ] 没有正在运行", task_id); Ok(false) } } -// 检查是否有正在运行的任务 +// 检查指定任务是否正在运行 #[tauri::command] -pub async fn is_execution_running(language: String) -> Result { +pub async fn is_execution_running(task_id: String) -> Result { let task_manager = init_task_manager(); let guard = task_manager.lock().await; - Ok(guard.contains_key(&language)) + Ok(guard.contains_key(&task_id)) } // 通用的代码执行函数 @@ -265,31 +265,43 @@ pub async fn execute_code( plugin_manager: State<'_, PluginManagerState>, app: AppHandle, ) -> Result { - info!("执行代码 -> 调用插件 [ {} ] 开始", request.language); - - // 先停止之前可能正在运行的任务 - let _ = stop_execution(request.language.clone()).await; + let task_id = request.task_id.clone(); + info!( + "执行代码 -> 调用插件 [ {} ] 任务 [ {} ] 开始", + request.language, task_id + ); let manager = plugin_manager.lock().await; let plugin = manager .get_plugin(&request.language) .ok_or_else(|| format!("Unsupported language: {}", request.language))?; - // 使用 .codeforge/cache/plugin/ 目录 - let temp_dir = get_codeforge_cache_dir(&request.language)?; - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - let file_work = format!("Codeforge_{}_{}", request.language, timestamp); - let work_dir = temp_dir.join(&file_work); - fs::create_dir_all(&work_dir).map_err(|e| format!("创建工作目录失败: {}", e))?; - let file_name = format!("{}.{}", file_work, plugin.get_file_extension()); - let file_path = work_dir.join(&file_name); - - // 写入代码到临时文件 - fs::write(&file_path, &request.code) - .map_err(|e| format!("Failed to write temporary file: {}", e))?; + // 决定运行的文件与工作目录: + // - 提供了 file_path:就地运行该文件,工作目录为其所在目录(多文件 import/相对路径正确) + // - 否则:写入 .codeforge/cache/plugins/ 临时目录后运行 + let (file_path, cwd): (PathBuf, Option) = if let Some(fp) = request.file_path.clone() { + let p = PathBuf::from(&fp); + let dir = p.parent().map(|d| d.to_path_buf()); + (p, dir) + } else { + let temp_dir = get_codeforge_cache_dir(&request.language)?; + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + let file_work = format!("Codeforge_{}_{}", request.language, timestamp); + let work_dir = temp_dir.join(&file_work); + fs::create_dir_all(&work_dir).map_err(|e| format!("创建工作目录失败: {}", e))?; + let file_name = format!("{}.{}", file_work, plugin.get_file_extension()); + let fp = work_dir.join(&file_name); + + // 写入代码到临时文件 + fs::write(&fp, &request.code) + .map_err(|e| format!("Failed to write temporary file: {}", e))?; + + let home = plugin.get_execute_home().map(PathBuf::from); + (fp, home) + }; let _processed_code = plugin .pre_execute_hook(&request.code, file_path.to_str().unwrap()) @@ -316,7 +328,8 @@ pub async fn execute_code( let _ = app.emit( "code-execution-start", serde_json::json!({ - "language": request.language + "language": request.language, + "task_id": task_id }), ); @@ -327,9 +340,9 @@ pub async fn execute_code( .stdout(Stdio::piped()) .stderr(Stdio::piped()); - // 如果插件有 execute_home,设置工作目录 - if let Some(execute_home) = plugin.get_execute_home() { - command.current_dir(&execute_home); + // 设置工作目录(就地运行为文件目录,否则为插件 execute_home) + if let Some(dir) = &cwd { + command.current_dir(dir); } let mut child = match command.spawn() { @@ -346,6 +359,7 @@ pub async fn execute_code( "code-execution-complete", serde_json::json!({ "language": request.language, + "task_id": task_id, "success": false }), ); @@ -366,7 +380,7 @@ pub async fn execute_code( { let mut guard = task_manager.lock().await; guard.insert( - request.language.clone(), + task_id.clone(), ExecutionTask { language: request.language.clone(), process_id: child.id(), @@ -422,13 +436,14 @@ pub async fn execute_code( // 从任务管理器中移除 { let mut guard = task_manager.lock().await; - guard.remove(&request.language); + guard.remove(&task_id); } let _ = app.emit( "code-execution-stopped", serde_json::json!({ - "language": request.language + "language": request.language, + "task_id": task_id }), ); @@ -445,13 +460,14 @@ pub async fn execute_code( // 从任务管理器中移除 { let mut guard = task_manager.lock().await; - guard.remove(&request.language); + guard.remove(&task_id); } let _ = app.emit( "code-execution-timeout", serde_json::json!({ - "language": request.language + "language": request.language, + "task_id": task_id }), ); @@ -471,7 +487,8 @@ pub async fn execute_code( serde_json::json!({ "type": "stdout", "content": line, - "language": request.language + "language": request.language, + "task_id": task_id }), ); } @@ -486,7 +503,8 @@ pub async fn execute_code( serde_json::json!({ "type": "stderr", "content": line, - "language": request.language + "language": request.language, + "task_id": task_id }), ); } @@ -503,7 +521,8 @@ pub async fn execute_code( serde_json::json!({ "type": "stdout", "content": line, - "language": request.language + "language": request.language, + "task_id": task_id }), ); } @@ -516,7 +535,8 @@ pub async fn execute_code( serde_json::json!({ "type": "stderr", "content": line, - "language": request.language + "language": request.language, + "task_id": task_id }), ); } @@ -533,7 +553,7 @@ pub async fn execute_code( // 从任务管理器中移除 { let mut guard = task_manager.lock().await; - guard.remove(&request.language); + guard.remove(&task_id); } let mut result = ExecutionResult { @@ -557,7 +577,8 @@ pub async fn execute_code( serde_json::json!({ "type": "stdout", "content": "代码执行成功 (无输出)", - "language": request.language + "language": request.language, + "task_id": task_id }), ); } @@ -566,6 +587,7 @@ pub async fn execute_code( "code-execution-complete", serde_json::json!({ "language": request.language, + "task_id": task_id, "success": result.success, "execution_time": result.execution_time }), @@ -588,13 +610,14 @@ pub async fn execute_code( // 从任务管理器中移除 { let mut guard = task_manager.lock().await; - guard.remove(&request.language); + guard.remove(&task_id); } let _ = app.emit( "code-execution-error", serde_json::json!({ "language": request.language, + "task_id": task_id, "error": e.to_string() }), ); diff --git a/src-tauri/src/plugins/mod.rs b/src-tauri/src/plugins/mod.rs index da2e1d9..d64bb7d 100644 --- a/src-tauri/src/plugins/mod.rs +++ b/src-tauri/src/plugins/mod.rs @@ -20,6 +20,10 @@ pub struct ExecutionResult { pub struct CodeExecutionRequest { pub code: String, pub language: String, + // 本次执行的唯一标识,用于事件路由(支持多标签并发运行) + pub task_id: String, + // 关联的本地文件路径;存在则就地运行该文件(工作目录为其所在目录) + pub file_path: Option, } #[derive(Debug, Serialize, Deserialize)] diff --git a/src/App.vue b/src/App.vue index b8996fc..136d527 100644 --- a/src/App.vue +++ b/src/App.vue @@ -8,7 +8,7 @@ :sidebar-visible="sidebarVisible" @toggle-sidebar="toggleSidebar" @run-code="handleRunCode" - @stop-code="() => stopCode(currentLanguage)" + @stop-code="stopCode" @language-change="onLanguageChange" @layout-change="handleLayoutChange" @open-file="handleOpenFileClick" @@ -432,12 +432,28 @@ const handleLayoutChange = (mode: LayoutMode) => { } } -// 包装运行:仅编辑器模式下点击运行时自动展开控制台 -const handleRunCode = () => { +// 包装运行:仅编辑器模式下点击运行时自动展开控制台;关联文件则就地运行 +const handleRunCode = async () => { if (layoutMode.value === 'editor') { showConsole.value = true } - runCode(currentLanguage.value, envInfo.value.installed, envInfo.value.language) + + const base = { + language: currentLanguage.value, + envInstalled: envInfo.value.installed, + envLanguage: envInfo.value.language + } + + // 关联了本地文件:有改动先保存,再就地运行(工作目录为文件所在目录) + if (currentFilePath.value) { + if (isDirty.value) { + await saveFile() + } + runCode({...base, filePath: currentFilePath.value}) + } + else { + runCode(base) + } } const handleSettingsChanged = async (config: any) => { @@ -486,7 +502,6 @@ const {initializeEventListeners, cleanupEventListeners} = useEventManager({ isRunning, isSuccess, lastExecutionTime, - currentLanguage, toast, handleRealtimeOutput, handleExecutionComplete, diff --git a/src/composables/useCodeExecution.ts b/src/composables/useCodeExecution.ts index 63ac460..50614c3 100644 --- a/src/composables/useCodeExecution.ts +++ b/src/composables/useCodeExecution.ts @@ -10,16 +10,32 @@ export function useCodeExecution(toast: any) const isSuccess = ref(false) const lastExecutionTime = ref(0) + // 当前运行任务的唯一标识,用于事件路由(支持多标签并发运行) + const currentTaskId = ref(null) + // 实时输出相关 const realTimeOutput = ref('') const realTimeStderr = ref('') - const runCode = async (currentLanguage: string, envInstalled: boolean, envLanguage: string) => { + interface RunOptions + { + language: string + envInstalled: boolean + envLanguage: string + filePath?: string | null + } + + const runCode = async (options: RunOptions) => { + const {language, envInstalled, envLanguage, filePath} = options if (!envInstalled) { toast.error(`${ envLanguage } 环境未安装`) return } + // 生成本次运行的 task_id,事件按它路由 + const taskId = crypto.randomUUID() + currentTaskId.value = taskId + isRunning.value = true // 清空所有输出 @@ -33,7 +49,9 @@ export function useCodeExecution(toast: any) const result: ExecutionResult = await invoke('execute_code', { request: { code: code.value, - language: currentLanguage + language, + task_id: taskId, + file_path: filePath || null } }) @@ -54,14 +72,14 @@ export function useCodeExecution(toast: any) } } - const stopCode = async (currentLanguage: string) => { - if (!isRunning.value) { + const stopCode = async () => { + if (!isRunning.value || !currentTaskId.value) { return } try { const result = await invoke('stop_execution', { - language: currentLanguage + taskId: currentTaskId.value }) if (result) { @@ -85,9 +103,9 @@ export function useCodeExecution(toast: any) } // 处理实时输出 - const handleRealtimeOutput = (currentLanguage: string, data: any) => { - // 只处理当前语言的输出 - if (data.language !== currentLanguage) { + const handleRealtimeOutput = (data: any) => { + // 只处理当前任务的输出 + if (data.task_id !== currentTaskId.value) { return } @@ -114,8 +132,8 @@ export function useCodeExecution(toast: any) } // 处理执行完成 - const handleExecutionComplete = (currentLanguage: string, data: any) => { - if (data.language === currentLanguage) { + const handleExecutionComplete = (data: any) => { + if (data.task_id === currentTaskId.value) { isRunning.value = false isSuccess.value = data.success if (data.execution_time) { @@ -125,8 +143,8 @@ export function useCodeExecution(toast: any) } // 处理执行停止 - const handleExecutionStopped = (currentLanguage: string, data: any) => { - if (data.language === currentLanguage) { + const handleExecutionStopped = (data: any) => { + if (data.task_id === currentTaskId.value) { isRunning.value = false output.value += '\n\n🛑 代码执行已被用户停止' toast.warning('代码执行已停止') @@ -134,8 +152,8 @@ export function useCodeExecution(toast: any) } // 处理执行超时 - const handleExecutionTimeout = (currentLanguage: string, data: any) => { - if (data.language === currentLanguage) { + const handleExecutionTimeout = (data: any) => { + if (data.task_id === currentTaskId.value) { isRunning.value = false output.value += '\n\n⚠️ 代码执行超时(30秒)' toast.error('代码执行超时') @@ -143,8 +161,8 @@ export function useCodeExecution(toast: any) } // 处理执行错误 - const handleExecutionError = (currentLanguage: string, data: any) => { - if (data.language === currentLanguage) { + const handleExecutionError = (data: any) => { + if (data.task_id === currentTaskId.value) { isRunning.value = false output.value += `\n\n❌ 执行错误: ${ data.error }` toast.error('代码执行出错') @@ -157,6 +175,7 @@ export function useCodeExecution(toast: any) isRunning, isSuccess, lastExecutionTime, + currentTaskId, runCode, stopCode, clearOutput, diff --git a/src/composables/useEventManager.ts b/src/composables/useEventManager.ts index edddc2b..c44fafc 100644 --- a/src/composables/useEventManager.ts +++ b/src/composables/useEventManager.ts @@ -11,13 +11,12 @@ interface EventManagerOptions isRunning: Ref isSuccess: Ref lastExecutionTime: Ref - currentLanguage: Ref toast: any - handleRealtimeOutput: (currentLanguage: string, data: any) => void - handleExecutionComplete: (currentLanguage: string, data: any) => void - handleExecutionStopped: (currentLanguage: string, data: any) => void - handleExecutionTimeout: (currentLanguage: string, data: any) => void - handleExecutionError: (currentLanguage: string, data: any) => void + handleRealtimeOutput: (data: any) => void + handleExecutionComplete: (data: any) => void + handleExecutionStopped: (data: any) => void + handleExecutionTimeout: (data: any) => void + handleExecutionError: (data: any) => void } export function useEventManager(options: EventManagerOptions) @@ -26,7 +25,6 @@ export function useEventManager(options: EventManagerOptions) showAbout, showSettings, showUpdate, - currentLanguage, handleRealtimeOutput, handleExecutionComplete, handleExecutionStopped, @@ -48,38 +46,28 @@ export function useEventManager(options: EventManagerOptions) // 处理实时输出 const handleRealtimeOutputWrapper = (event: any) => { const data: CodeOutputEvent = event.payload - console.log('实时输出:', data) - handleRealtimeOutput(currentLanguage.value, data) + handleRealtimeOutput(data) } // 处理执行状态事件 - const handleExecutionStart = (event: any) => { - const data = event.payload - if (data.language === currentLanguage.value) { - console.log('代码开始执行') - } + const handleExecutionStart = (_event: any) => { + console.log('代码开始执行') } const handleExecutionCompleteWrapper = (event: any) => { - const data = event.payload - console.log('代码执行完成') - handleExecutionComplete(currentLanguage.value, data) + handleExecutionComplete(event.payload) } const handleExecutionStoppedWrapper = (event: any) => { - const data = event.payload - console.log('代码执行已停止') - handleExecutionStopped(currentLanguage.value, data) + handleExecutionStopped(event.payload) } const handleExecutionTimeoutWrapper = (event: any) => { - const data = event.payload - handleExecutionTimeout(currentLanguage.value, data) + handleExecutionTimeout(event.payload) } const handleExecutionErrorWrapper = (event: any) => { - const data = event.payload - handleExecutionError(currentLanguage.value, data) + handleExecutionError(event.payload) } const initializeEventListeners = async () => { From 449438bdadcc2411a5b3655f15d756dc19040940 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Wed, 3 Jun 2026 19:50:57 +0800 Subject: [PATCH 03/29] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=9C=AA=E4=BF=9D=E5=AD=98=E6=96=87=E4=BB=B6=E7=AD=96?= =?UTF-8?q?=E7=95=A5=E8=AE=BE=E7=BD=AE(=E8=87=AA=E5=8A=A8=E4=BF=9D?= =?UTF-8?q?=E5=AD=98/=E8=AF=A2=E9=97=AE/=E8=BF=90=E8=A1=8C=E5=89=AF?= =?UTF-8?q?=E6=9C=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/config.rs | 26 +++++++----- src/App.vue | 67 ++++++++++++++++++++++++------ src/components/setting/Editor.vue | 10 +++++ src/composables/useEditorConfig.ts | 1 + src/types/app.ts | 3 ++ 5 files changed, 84 insertions(+), 23 deletions(-) diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 74a9c81..6f742db 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -12,17 +12,18 @@ static CONFIG_MANAGER: Mutex> = Mutex::new(None); #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EditorConfig { - pub indent_with_tab: Option, // 是否使用 tab 缩进 - pub tab_size: Option, // tab 缩进, 空格数,默认为 2 - pub theme: Option, // 编辑器主题 - pub font_size: Option, // 编辑器字体大小 - pub font_family: Option, // 编辑器字体 - pub show_line_numbers: Option, // 是否显示行号 - pub show_function_help: Option, // 是否显示函数帮助 - pub space_dot_omission: Option, // 是否显示空格省略 - pub layout: Option, // 编辑器/控制台布局: horizontal | vertical | editor - pub last_direction: Option, // 仅编辑器模式下控制台弹出方向: horizontal | vertical - pub max_open_file_size: Option, // 打开文件大小上限(MB),超过则拒绝打开 + pub indent_with_tab: Option, // 是否使用 tab 缩进 + pub tab_size: Option, // tab 缩进, 空格数,默认为 2 + pub theme: Option, // 编辑器主题 + pub font_size: Option, // 编辑器字体大小 + pub font_family: Option, // 编辑器字体 + pub show_line_numbers: Option, // 是否显示行号 + pub show_function_help: Option, // 是否显示函数帮助 + pub space_dot_omission: Option, // 是否显示空格省略 + pub layout: Option, // 编辑器/控制台布局: horizontal | vertical | editor + pub last_direction: Option, // 仅编辑器模式下控制台弹出方向: horizontal | vertical + pub max_open_file_size: Option, // 打开文件大小上限(MB),超过则拒绝打开 + pub run_save_strategy: Option, // 运行未保存文件策略: auto-save | ask | temp-copy } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -71,6 +72,7 @@ impl Default for AppConfig { layout: Some("horizontal".to_string()), last_direction: Some("horizontal".to_string()), max_open_file_size: Some(5), + run_save_strategy: Some("auto-save".to_string()), }), environment_mirror: Some(EnvironmentMirrorConfig { enabled: Some(false), @@ -140,6 +142,7 @@ impl ConfigManager { layout: Some("horizontal".to_string()), last_direction: Some("horizontal".to_string()), max_open_file_size: Some(5), + run_save_strategy: Some("auto-save".to_string()), }); println!("读取配置 -> 添加默认 editor 配置"); } @@ -262,6 +265,7 @@ impl ConfigManager { layout: Some("horizontal".to_string()), last_direction: Some("horizontal".to_string()), max_open_file_size: Some(5), + run_save_strategy: Some("auto-save".to_string()), }), environment_mirror: Some(EnvironmentMirrorConfig { enabled: Some(false), diff --git a/src/App.vue b/src/App.vue index 136d527..a0dde8d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -145,6 +145,20 @@ :supported-languages="supportedLanguages" @restore="restoreHistoryItem"/> + + +
+

+ 当前文件 {{ currentFileName }} 有未保存的修改,如何运行? +

+
+ + + +
+
+
+ @@ -174,6 +188,8 @@ import {useWorkspace} from './composables/useWorkspace' import EditorTabs from './components/EditorTabs.vue' import Sidebar from './components/Sidebar.vue' import LargeFileViewer from './components/LargeFileViewer.vue' +import Modal from './ui/Modal.vue' +import Button from './ui/Button.vue' import ExecutionHistory from './components/ExecutionHistory.vue' import {open as openDialog} from '@tauri-apps/plugin-dialog' import {invoke} from '@tauri-apps/api/core' @@ -432,30 +448,57 @@ const handleLayoutChange = (mode: LayoutMode) => { } } -// 包装运行:仅编辑器模式下点击运行时自动展开控制台;关联文件则就地运行 +const buildRunBase = () => ({ + language: currentLanguage.value, + envInstalled: envInfo.value.installed, + envLanguage: envInfo.value.language +}) + +// 运行未保存文件的询问弹窗 +const showRunPrompt = ref(false) + +// 包装运行:仅编辑器模式下点击运行时自动展开控制台;关联文件则按策略就地运行 const handleRunCode = async () => { if (layoutMode.value === 'editor') { showConsole.value = true } - const base = { - language: currentLanguage.value, - envInstalled: envInfo.value.installed, - envLanguage: envInfo.value.language + // 草稿(无关联文件):临时目录运行 + if (!currentFilePath.value) { + runCode(buildRunBase()) + return + } + // 无改动:直接就地运行 + if (!isDirty.value) { + runCode({...buildRunBase(), filePath: currentFilePath.value}) + return } - // 关联了本地文件:有改动先保存,再就地运行(工作目录为文件所在目录) - if (currentFilePath.value) { - if (isDirty.value) { - await saveFile() - } - runCode({...base, filePath: currentFilePath.value}) + // 有未保存改动:按设置的策略处理 + const strategy = editorConfig.value?.run_save_strategy || 'auto-save' + if (strategy === 'temp-copy') { + runCode(buildRunBase()) // 跑当前未保存内容的临时副本 + } + else if (strategy === 'ask') { + showRunPrompt.value = true } else { - runCode(base) + await saveFile() + runCode({...buildRunBase(), filePath: currentFilePath.value}) } } +const promptSaveAndRun = async () => { + showRunPrompt.value = false + await saveFile() + runCode({...buildRunBase(), filePath: currentFilePath.value}) +} + +const promptRunCopy = () => { + showRunPrompt.value = false + runCode(buildRunBase()) +} + const handleSettingsChanged = async (config: any) => { console.log('主组件接收到设置变更:', config) setTimeout(() => { diff --git a/src/components/setting/Editor.vue b/src/components/setting/Editor.vue index 6cf51ba..c88ff44 100644 --- a/src/components/setting/Editor.vue +++ b/src/components/setting/Editor.vue @@ -35,6 +35,10 @@ + @@ -57,6 +61,12 @@ const emit = defineEmits<{ 'error': [message: string] }>() +const runSaveStrategyOptions = [ + {label: '自动保存后运行', value: 'auto-save'}, + {label: '每次询问', value: 'ask'}, + {label: '运行副本(不保存)', value: 'temp-copy'} +] + const { editorConfig, themeOptions, diff --git a/src/composables/useEditorConfig.ts b/src/composables/useEditorConfig.ts index a2937aa..f7e2670 100644 --- a/src/composables/useEditorConfig.ts +++ b/src/composables/useEditorConfig.ts @@ -161,6 +161,7 @@ export function useEditorConfig(emit?: any) layout: 'horizontal', last_direction: 'horizontal', max_open_file_size: 5, + run_save_strategy: 'auto-save', } } diff --git a/src/types/app.ts b/src/types/app.ts index b732cb2..7027998 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -59,8 +59,11 @@ export interface EditorConfig layout?: LayoutMode last_direction?: SplitDirection max_open_file_size?: number + run_save_strategy?: RunSaveStrategy } +export type RunSaveStrategy = 'auto-save' | 'ask' | 'temp-copy' + export type SplitDirection = 'horizontal' | 'vertical' export type LayoutMode = SplitDirection | 'editor' From b10879c4a290b8877a1bacc11dfbed151f72bb6b Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Wed, 3 Jun 2026 19:57:24 +0800 Subject: [PATCH 04/29] =?UTF-8?q?feat:=20=E5=B0=86=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E7=AD=96=E7=95=A5=E4=B8=8E=E6=96=87=E4=BB=B6=E5=A4=A7=E5=B0=8F?= =?UTF-8?q?=E4=B8=8A=E9=99=90=E7=A7=BB=E8=87=B3=E9=80=9A=E7=94=A8=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 8 +++- src/components/setting/Editor.vue | 16 +------- src/components/setting/General.vue | 63 +++++++++++++++++++++++++++++- 3 files changed, 71 insertions(+), 16 deletions(-) diff --git a/src/App.vue b/src/App.vue index a0dde8d..36bfb2a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -135,7 +135,7 @@ - + @@ -499,6 +499,12 @@ const promptRunCopy = () => { runCode(buildRunBase()) } +// 设置关闭后刷新缓存的编辑器配置,使运行策略/文件大小上限等即时生效 +const onSettingsClose = async () => { + closeSettings() + await loadEditorConfig() +} + const handleSettingsChanged = async (config: any) => { console.log('主组件接收到设置变更:', config) setTimeout(() => { diff --git a/src/components/setting/Editor.vue b/src/components/setting/Editor.vue index c88ff44..c26545e 100644 --- a/src/components/setting/Editor.vue +++ b/src/components/setting/Editor.vue @@ -31,20 +31,14 @@ - - - + + From bf2d963433b7c25b3d3745e874131caff5a3b35c Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Wed, 3 Jun 2026 19:59:15 +0800 Subject: [PATCH 05/29] =?UTF-8?q?feat:=20=E5=B7=A5=E5=85=B7=E6=A0=8F?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E6=8C=89=E9=92=AE=E6=94=B9=E7=94=A8=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E7=9A=84=20Tooltip=20=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/AppHeader.vue | 42 +++++++++++++++++++++--------------- src/ui/Tooltip.vue | 25 +++++++++++++++++++++ 2 files changed, 50 insertions(+), 17 deletions(-) create mode 100644 src/ui/Tooltip.vue diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue index 569eb67..0f869a2 100644 --- a/src/components/AppHeader.vue +++ b/src/components/AppHeader.vue @@ -28,33 +28,40 @@ - + + +
- + + + + + + + + + + +
+

+ 确定删除 {{ deleteModal.node?.name }}{{ deleteModal.node?.is_dir ? '(及其内容)' : '' }}?此操作不可恢复。 +

+
+ + +
+
+
From 9c3b88d7f6c787d52d82ea6ba734389527fd8d1b Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Thu, 4 Jun 2026 09:26:46 +0800 Subject: [PATCH 11/29] =?UTF-8?q?feat:=20=E7=9B=91=E5=90=AC=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=A4=B9=E5=8F=98=E5=8C=96=E8=87=AA=E5=8A=A8=E5=88=B7?= =?UTF-8?q?=E6=96=B0=E6=96=87=E4=BB=B6=E6=A0=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/Cargo.lock | 159 +++++++++++++++++++++++++++--------- src-tauri/Cargo.toml | 1 + src-tauri/src/filesystem.rs | 26 ++++++ src-tauri/src/main.rs | 5 +- src/components/Sidebar.vue | 39 ++++++++- 5 files changed, 187 insertions(+), 43 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6fe8bda..9c21961 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -14,6 +14,7 @@ dependencies = [ "flate2", "futures-util", "log", + "notify", "regex", "reqwest 0.11.27", "rfd 0.15.4", @@ -387,11 +388,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -507,7 +508,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "cairo-sys-rs", "glib", "libc", @@ -710,7 +711,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "core-foundation 0.10.1", "core-graphics-types", "foreign-types 0.5.0", @@ -723,7 +724,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "core-foundation 0.10.1", "core-graphics-types", "foreign-types 0.5.0", @@ -736,7 +737,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "core-foundation 0.10.1", "libc", ] @@ -970,7 +971,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "block2 0.6.1", "libc", "objc2 0.6.4", @@ -1335,6 +1336,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -1598,7 +1608,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "futures-channel", "futures-core", "futures-executor", @@ -2142,6 +2152,26 @@ dependencies = [ "cfb", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "inout" version = "0.1.4" @@ -2157,7 +2187,7 @@ version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "cfg-if", "libc", ] @@ -2288,11 +2318,31 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "serde", "unicode-segmentation", ] +[[package]] +name = "kqueue" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.12.1", + "libc", +] + [[package]] name = "libappindicator" version = "0.9.0" @@ -2348,7 +2398,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "libc", "redox_syscall", ] @@ -2455,6 +2505,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.0.4" @@ -2510,7 +2572,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "jni-sys", "log", "ndk-sys", @@ -2540,13 +2602,32 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "cfg-if", "cfg_aliases", "libc", "memoffset", ] +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.12.1", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio 0.8.11", + "walkdir", + "windows-sys 0.48.0", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2616,7 +2697,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "block2 0.6.1", "objc2 0.6.4", "objc2-core-foundation", @@ -2629,7 +2710,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "objc2 0.6.4", "objc2-foundation 0.3.1", ] @@ -2650,7 +2731,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "dispatch2", "objc2 0.6.4", ] @@ -2661,7 +2742,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "dispatch2", "objc2 0.6.4", "objc2-core-foundation", @@ -2709,7 +2790,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -2721,7 +2802,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "block2 0.6.1", "libc", "objc2 0.6.4", @@ -2734,7 +2815,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "objc2 0.6.4", "objc2-core-foundation", ] @@ -2745,7 +2826,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -2757,7 +2838,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -2770,7 +2851,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.1", @@ -2782,7 +2863,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "block2 0.6.1", "objc2 0.6.4", "objc2-cloud-kit", @@ -2812,7 +2893,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "block2 0.6.1", "objc2 0.6.4", "objc2-app-kit", @@ -2853,7 +2934,7 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "cfg-if", "foreign-types 0.3.2", "libc", @@ -3107,7 +3188,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "crc32fast", "fdeflate", "flate2", @@ -3299,7 +3380,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", ] [[package]] @@ -3492,7 +3573,7 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -3527,7 +3608,7 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "errno", "libc", "linux-raw-sys", @@ -3642,7 +3723,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -3665,7 +3746,7 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "cssparser", "derive_more", "log", @@ -4171,7 +4252,7 @@ version = "0.35.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "block2 0.6.1", "core-foundation 0.10.1", "core-graphics 0.25.0", @@ -4661,7 +4742,7 @@ dependencies = [ "bytes", "io-uring", "libc", - "mio", + "mio 1.0.4", "pin-project-lite", "slab", "socket2 0.6.0", @@ -4831,7 +4912,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "bytes", "futures-util", "http 1.3.1", @@ -5226,7 +5307,7 @@ version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "rustix", "wayland-backend", "wayland-scanner", @@ -5238,7 +5319,7 @@ version = "0.32.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", "wayland-backend", "wayland-client", "wayland-scanner", @@ -5887,7 +5968,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.12.1", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 38999ab..8ffc93d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -40,3 +40,4 @@ flate2 = "1.0" tar = "0.4" xz2 = "0.1" zstd = "0.13" +notify = "6" diff --git a/src-tauri/src/filesystem.rs b/src-tauri/src/filesystem.rs index 8cdd9bf..229b7c1 100644 --- a/src-tauri/src/filesystem.rs +++ b/src-tauri/src/filesystem.rs @@ -1,3 +1,4 @@ +use notify::{RecommendedWatcher, RecursiveMode, Watcher}; use serde::Serialize; use std::collections::HashMap; use std::fs; @@ -5,6 +6,7 @@ use std::io::{BufRead, BufReader, Read, Seek, SeekFrom}; use std::path::Path; use std::sync::Mutex; use std::time::SystemTime; +use tauri::{AppHandle, Emitter}; #[derive(Serialize)] pub struct FileNode { @@ -115,6 +117,30 @@ pub fn delete_path(path: String) -> Result<(), String> { } } +// 全局目录监听器(保持存活;切换目录时替换旧的) +static WATCHER: Mutex> = Mutex::new(None); + +/// 监听目录变化,变化时向前端发送 `fs-changed` 事件 +#[tauri::command] +pub fn watch_directory(path: String, app: AppHandle) -> Result<(), String> { + let app_handle = app.clone(); + let mut watcher = notify::recommended_watcher(move |res: notify::Result| { + if res.is_ok() { + let _ = app_handle.emit("fs-changed", ()); + } + }) + .map_err(|e| format!("创建文件监听失败: {}", e))?; + + watcher + .watch(Path::new(&path), RecursiveMode::Recursive) + .map_err(|e| format!("监听目录失败: {}", e))?; + + // 替换旧监听器(drop 旧的即停止监听) + let mut guard = WATCHER.lock().map_err(|_| "监听锁错误".to_string())?; + *guard = Some(watcher); + Ok(()) +} + /// 在系统文件管理器中显示该路径 #[tauri::command] pub fn reveal_path(path: String) -> Result<(), String> { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 0d601d2..6ce1b04 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -41,7 +41,7 @@ use crate::execution::{ }; use crate::filesystem::{ create_directory, create_file, delete_path, get_text_file_meta, read_directory_tree, - read_file_lines, read_file_text, rename_path, reveal_path, write_file_text, + read_file_lines, read_file_text, rename_path, reveal_path, watch_directory, write_file_text, }; use crate::plugin::{get_info, get_supported_languages}; use crate::setup::app::get_app_info; @@ -166,7 +166,8 @@ fn main() { create_directory, rename_path, delete_path, - reveal_path + reveal_path, + watch_directory ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/components/Sidebar.vue b/src/components/Sidebar.vue index 2ee64be..7eb867f 100644 --- a/src/components/Sidebar.vue +++ b/src/components/Sidebar.vue @@ -90,8 +90,9 @@ diff --git a/src/composables/useWorkspace.ts b/src/composables/useWorkspace.ts index aa60a9a..54e07be 100644 --- a/src/composables/useWorkspace.ts +++ b/src/composables/useWorkspace.ts @@ -136,6 +136,51 @@ export function useWorkspace(deps: WorkspaceDeps) } } + // 关闭除 id 外的所有标签 + const closeOthers = (id: string) => { + captureToActive() + const keep = tabs.value.find(t => t.id === id) + if (!keep) { + return + } + tabs.value = [keep] + if (activeTabId.value !== id) { + activeTabId.value = id + loadTab(keep) + } + } + + // 关闭 id 右侧的所有标签 + const closeToRight = (id: string) => { + const idx = tabs.value.findIndex(t => t.id === id) + if (idx === -1) { + return + } + captureToActive() + const activeRemoved = tabs.value.slice(idx + 1).some(t => t.id === activeTabId.value) + tabs.value = tabs.value.slice(0, idx + 1) + if (activeRemoved) { + activeTabId.value = id + const keep = tabs.value[idx] + if (keep) { + loadTab(keep) + } + } + } + + // 拖拽排序:把 fromId 移动到 toId 的位置 + const moveTab = (fromId: string, toId: string) => { + const from = tabs.value.findIndex(t => t.id === fromId) + const to = tabs.value.findIndex(t => t.id === toId) + if (from === -1 || to === -1 || from === to) { + return + } + const arr = [...tabs.value] + const [moved] = arr.splice(from, 1) + arr.splice(to, 0, moved) + tabs.value = arr + } + // 路径是否等于 target 或在其目录下 const isUnder = (p: string, target: string) => p === target || (p.startsWith(target) && (p[target.length] === '/' || p[target.length] === '\\')) @@ -188,6 +233,9 @@ export function useWorkspace(deps: WorkspaceDeps) switchTab, newTab, closeTab, + closeOthers, + closeToRight, + moveTab, updateTabPath, detachTabPath, isActiveReusableScratch From 2cd639789f30b672c1f3b0daab1d93288256651d Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Thu, 4 Jun 2026 10:07:50 +0800 Subject: [PATCH 17/29] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E6=8B=96=E6=8B=BD=E6=8E=92=E5=BA=8F=E6=97=A0=E6=95=88?= =?UTF-8?q?(setData=20+=20=E5=85=B3=E9=97=AD=E7=AA=97=E5=8F=A3=E7=BA=A7?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E6=8B=96=E6=94=BE=E6=8B=A6=E6=88=AA)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/tauri.conf.json | 1 + src/components/EditorTabs.vue | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 4936414..23854b3 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -19,6 +19,7 @@ "height": 1200, "center": true, "devtools": true, + "dragDropEnabled": false, "additionalBrowserArgs": "--disable-context-menu" } ], diff --git a/src/components/EditorTabs.vue b/src/components/EditorTabs.vue index b917dfd..00810a7 100644 --- a/src/components/EditorTabs.vue +++ b/src/components/EditorTabs.vue @@ -90,6 +90,8 @@ const onDragStart = (id: string, e: DragEvent) => { draggingId.value = id if (e.dataTransfer) { e.dataTransfer.effectAllowed = 'move' + // 部分 webview 需要写入数据才会真正启动拖拽 + e.dataTransfer.setData('text/plain', id) } } const onDrop = (id: string) => { From 4431f698d5d555097a9b56efbf39fddb6f50c2cf Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Thu, 4 Jun 2026 10:14:32 +0800 Subject: [PATCH 18/29] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E4=B8=BA?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E7=A8=8B=E5=BA=8F=E6=8F=90=E4=BE=9B=E6=A0=87?= =?UTF-8?q?=E5=87=86=E8=BE=93=E5=85=A5=E4=B8=8E=E8=BF=90=E8=A1=8C=E5=8F=82?= =?UTF-8?q?=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/execution.rs | 22 ++++++++++++++++++++-- src-tauri/src/plugins/mod.rs | 4 ++++ src/App.vue | 23 +++++++++++++++++++++-- src/composables/useCodeExecution.ts | 8 ++++++-- 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/execution.rs b/src-tauri/src/execution.rs index ba59e0d..3983a60 100644 --- a/src-tauri/src/execution.rs +++ b/src-tauri/src/execution.rs @@ -4,7 +4,7 @@ use rusqlite::{Connection, params}; use serde::Serialize; use std::collections::HashMap; use std::fs; -use std::io::{BufRead, BufReader}; +use std::io::{BufRead, BufReader, Write}; use std::path::PathBuf; use std::process::{Command, Stdio}; use std::sync::{Arc, Mutex as StdMutex, OnceLock, mpsc}; @@ -316,7 +316,11 @@ pub async fn execute_code( let start_time = std::time::Instant::now(); let cmd = plugin.get_command(None, false, Some(file_path.to_string_lossy().to_string())); - let args = plugin.get_execute_args(file_path.to_str().unwrap()); + let mut args = plugin.get_execute_args(file_path.to_str().unwrap()); + // 追加用户自定义运行参数 + if let Some(extra) = &request.args { + args.extend(extra.iter().cloned()); + } info!( "执行代码 -> 调用插件 [ {} ] 执行命令 {} 携带参数 {}", request.language, @@ -340,6 +344,13 @@ pub async fn execute_code( .stdout(Stdio::piped()) .stderr(Stdio::piped()); + // 有标准输入则用管道写入,否则关闭 stdin 避免程序读取时挂起 + if request.stdin.is_some() { + command.stdin(Stdio::piped()); + } else { + command.stdin(Stdio::null()); + } + // 设置工作目录(就地运行为文件目录,否则为插件 execute_home) if let Some(dir) = &cwd { command.current_dir(dir); @@ -372,6 +383,13 @@ pub async fn execute_code( } }; + // 写入标准输入后关闭管道(让程序读到 EOF) + if let Some(input) = &request.stdin { + if let Some(mut stdin) = child.stdin.take() { + let _ = stdin.write_all(input.as_bytes()); + } + } + // 创建停止标志 let stop_flag = Arc::new(tokio::sync::Mutex::new(false)); diff --git a/src-tauri/src/plugins/mod.rs b/src-tauri/src/plugins/mod.rs index d64bb7d..99044cc 100644 --- a/src-tauri/src/plugins/mod.rs +++ b/src-tauri/src/plugins/mod.rs @@ -24,6 +24,10 @@ pub struct CodeExecutionRequest { pub task_id: String, // 关联的本地文件路径;存在则就地运行该文件(工作目录为其所在目录) pub file_path: Option, + // 传给程序的标准输入 + pub stdin: Option, + // 追加到运行命令后的参数 + pub args: Option>, } #[derive(Debug, Serialize, Deserialize)] diff --git a/src/App.vue b/src/App.vue index 4eca774..0475925 100644 --- a/src/App.vue +++ b/src/App.vue @@ -81,6 +81,18 @@ + +
+ +
+ + +
+
+ import {computed, onMounted, onUnmounted, ref, watch} from 'vue' -import {X} from 'lucide-vue-next' +import {ChevronRight, X} from 'lucide-vue-next' import {ExecutionResult, LayoutMode, SplitDirection} from './types/app.ts' import AppHeader from './components/AppHeader.vue' import CodeEditor from './components/CodeEditor.vue' @@ -546,10 +558,17 @@ const handleLayoutChange = (mode: LayoutMode) => { } } +// 运行输入:参数 + stdin +const showRunInput = ref(false) +const runArgs = ref('') +const runStdin = ref('') + const buildRunBase = () => ({ language: currentLanguage.value, envInstalled: envInfo.value.installed, - envLanguage: envInfo.value.language + envLanguage: envInfo.value.language, + args: runArgs.value.trim() ? runArgs.value.trim().split(/\s+/) : undefined, + stdin: runStdin.value || undefined }) // 运行未保存文件的询问弹窗 diff --git a/src/composables/useCodeExecution.ts b/src/composables/useCodeExecution.ts index 50614c3..2e21031 100644 --- a/src/composables/useCodeExecution.ts +++ b/src/composables/useCodeExecution.ts @@ -23,10 +23,12 @@ export function useCodeExecution(toast: any) envInstalled: boolean envLanguage: string filePath?: string | null + args?: string[] + stdin?: string } const runCode = async (options: RunOptions) => { - const {language, envInstalled, envLanguage, filePath} = options + const {language, envInstalled, envLanguage, filePath, args, stdin} = options if (!envInstalled) { toast.error(`${ envLanguage } 环境未安装`) return @@ -51,7 +53,9 @@ export function useCodeExecution(toast: any) code: code.value, language, task_id: taskId, - file_path: filePath || null + file_path: filePath || null, + args: args && args.length ? args : null, + stdin: stdin ? stdin : null } }) From 083180db5a7ed27f940a00d57621570c4f25c422 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Thu, 4 Jun 2026 10:22:03 +0800 Subject: [PATCH 19/29] =?UTF-8?q?fix:=20=E8=BF=90=E8=A1=8C=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E9=9D=A2=E6=9D=BF=E6=8F=90=E5=88=B0=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E6=A0=8F=E4=B8=8B=E6=96=B9=EF=BC=8C=E4=BB=BB=E4=BD=95=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E8=BF=90=E8=A1=8C=E5=89=8D=E9=83=BD=E5=8F=AF=E5=A1=AB?= =?UTF-8?q?=20stdin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/App.vue b/src/App.vue index 0475925..7237ca9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -18,6 +18,19 @@ @load-example="loadExample"> + +
+ +
+ + +
+
+