diff --git a/package.json b/package.json index 97d9af9..b3c0f68 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "codeforge", "private": true, - "version": "26.1.0", + "version": "26.2.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 90f98bf..f4f47d7 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "CodeForge" -version = "26.1.0" +version = "26.2.0" dependencies = [ "async-trait", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 36b63e5..2206837 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "CodeForge" -version = "26.1.0" +version = "26.2.0" description = "CodeForge 是一款轻量级、高性能的桌面代码执行器,专为开发者、学生和编程爱好者设计。" authors = ["devlive-community"] edition = "2024" diff --git a/src-tauri/src/lsp.rs b/src-tauri/src/lsp.rs index 233c87d..af6c8c0 100644 --- a/src-tauri/src/lsp.rs +++ b/src-tauri/src/lsp.rs @@ -5,13 +5,14 @@ use serde::Serialize; use std::collections::HashMap; use std::io::{BufReader, Read, Write}; use std::path::PathBuf; -use std::process::{Child, ChildStdin, Command, Stdio}; +use std::process::{Child, Command, Stdio}; use std::sync::Mutex as StdMutex; use tauri::{AppHandle, Emitter, State}; struct Server { child: Child, - stdin: ChildStdin, + // 写入通过独立线程,避免 stdin 写阻塞主线程(语言服务器索引时可能来不及读) + tx: std::sync::mpsc::Sender>, } pub struct LspState { @@ -27,9 +28,9 @@ impl LspState { } #[derive(Clone, Serialize)] -struct LspEvent { +struct LspBatch { language: String, - message: String, + messages: Vec, } /// 语言 -> (可执行名, 参数)。新增语言在此加一行。 @@ -49,6 +50,13 @@ fn server_cmd(language: &str) -> Option<(&'static str, Vec<&'static str>)> { "html" => Some(("vscode-html-language-server", vec!["--stdio"])), "css" => Some(("vscode-css-language-server", vec!["--stdio"])), "json" => Some(("vscode-json-language-server", vec!["--stdio"])), + "java" => Some(("jdtls", vec![])), + "kotlin" => Some(("kotlin-language-server", vec![])), + "swift" => Some(("sourcekit-lsp", vec![])), + "scala" => Some(("metals", vec![])), + "yaml" => Some(("yaml-language-server", vec!["--stdio"])), + "shell" => Some(("bash-language-server", vec!["start"])), + "haskell" => Some(("haskell-language-server-wrapper", vec!["--lsp"])), _ => None, } } @@ -106,6 +114,43 @@ fn server_defs() -> Vec<(&'static str, &'static str, &'static str, &'static str) "vscode-html-language-server", "npm i -g vscode-langservers-extracted", ), + ("java", "Java (jdtls)", "jdtls", "brew install jdtls"), + ( + "kotlin", + "Kotlin", + "kotlin-language-server", + "brew install kotlin-language-server", + ), + ( + "swift", + "Swift (sourcekit-lsp)", + "sourcekit-lsp", + "xcode-select --install", + ), + ( + "scala", + "Scala (metals)", + "metals", + "coursier install metals", + ), + ( + "yaml", + "YAML", + "yaml-language-server", + "npm i -g yaml-language-server", + ), + ( + "shell", + "Shell / Bash", + "bash-language-server", + "npm i -g bash-language-server", + ), + ( + "haskell", + "Haskell (HLS)", + "haskell-language-server-wrapper", + "ghcup install hls", + ), ] } @@ -308,21 +353,45 @@ pub fn lsp_start( let stdout = child.stdout.take().ok_or("无法获取 stdout")?; let stderr = child.stderr.take(); - // 读取线程:解析 Content-Length 帧,原样转发 JSON 给前端 - let app_reader = app.clone(); + // 读取线程:解析 Content-Length 帧,把消息体丢进 channel + let (msg_tx, msg_rx) = std::sync::mpsc::channel::(); let lang_reader = language.clone(); std::thread::spawn(move || { let mut reader = BufReader::new(stdout); while let Some(body) = read_message(&mut reader) { + if msg_tx.send(body).is_err() { + break; + } + } + // 发送端 drop → 下面的 emitter 收到 Err 后发 lsp:exit + let _ = &lang_reader; + }); + + // 发射线程:合批转发。索引时语言服务器会突发成千上万条通知, + // 逐条 emit 会让 webview 主线程被 IPC 反序列化压垮(编辑器/环境检查随之卡死)。 + // 这里把"此刻已排队"的消息一次性打包成一个事件,空闲时则单条即时下发。 + let app_reader = app.clone(); + let lang_emit = language.clone(); + std::thread::spawn(move || { + // 阻塞等待第一条;channel 关闭(语言服务器退出)时 recv 返回 Err,循环结束 + while let Ok(first) = msg_rx.recv() { + let mut batch = vec![first]; + // 排空当前已到达的消息,凑成一批(上限防止单批过大) + while let Ok(m) = msg_rx.try_recv() { + batch.push(m); + if batch.len() >= 512 { + break; + } + } let _ = app_reader.emit( - "lsp:message", - LspEvent { - language: lang_reader.clone(), - message: body, + "lsp:messages", + LspBatch { + language: lang_emit.clone(), + messages: batch, }, ); } - let _ = app_reader.emit("lsp:exit", lang_reader.clone()); + let _ = app_reader.emit("lsp:exit", lang_emit.clone()); }); // 排空 stderr,避免阻塞 @@ -337,11 +406,27 @@ pub fn lsp_start( }); } + // 写入线程:独占 stdin,从 channel 取帧写入。 + // 这样 lsp_send 只需把帧塞进 channel(瞬时返回),即便语言服务器索引时 + // 不读 stdin 导致管道写阻塞,也只阻塞这个后台线程,不会卡住主线程的命令循环。 + let (tx, rx) = std::sync::mpsc::channel::>(); + std::thread::spawn(move || { + let mut stdin = stdin; + while let Ok(frame) = rx.recv() { + if stdin.write_all(&frame).is_err() { + break; + } + if stdin.flush().is_err() { + break; + } + } + }); + state .servers .lock() .map_err(|e| e.to_string())? - .insert(language, Server { child, stdin }); + .insert(language, Server { child, tx }); Ok(true) } @@ -352,16 +437,16 @@ pub fn lsp_send( language: String, message: String, ) -> Result<(), String> { - let mut servers = state.servers.lock().map_err(|e| e.to_string())?; + let servers = state.servers.lock().map_err(|e| e.to_string())?; let server = servers - .get_mut(&language) + .get(&language) .ok_or_else(|| "语言服务器未启动".to_string())?; let frame = format!("Content-Length: {}\r\n\r\n{}", message.len(), message); + // 仅入队,不在持锁/主线程上做阻塞写 server - .stdin - .write_all(frame.as_bytes()) - .map_err(|e| e.to_string())?; - server.stdin.flush().map_err(|e| e.to_string())?; + .tx + .send(frame.into_bytes()) + .map_err(|_| "语言服务器写入通道已关闭".to_string())?; Ok(()) } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 405ce35..11ab8f6 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "CodeForge", - "version": "26.1.0", + "version": "26.2.0", "identifier": "org.devlive.codeforge", "build": { "beforeDevCommand": "pnpm dev", diff --git a/src/App.vue b/src/App.vue index 589c822..b4b79b2 100644 --- a/src/App.vue +++ b/src/App.vue @@ -263,7 +263,7 @@ @close="showTerminal = false; terminalMounted = false"/> - + @@ -360,18 +360,47 @@ @refresh="refreshGitStatus" @close="showGit = false"/> + + + + +
+
+ + +
+ + +
+
+ diff --git a/src/components/DiagnosticsPanel.vue b/src/components/DiagnosticsPanel.vue new file mode 100644 index 0000000..138b667 --- /dev/null +++ b/src/components/DiagnosticsPanel.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/components/GitPanel.vue b/src/components/GitPanel.vue index 8d44316..612f03e 100644 --- a/src/components/GitPanel.vue +++ b/src/components/GitPanel.vue @@ -67,11 +67,13 @@
- - - +
@@ -101,7 +103,9 @@ const status = ref({is_repo: false, branch: '', ahead: 0, behind: const branches = ref([]) const message = ref('') const loading = ref(false) -const busy = ref(false) +// 正在进行的提交/推送动作,用于按钮加载状态;busy 据此派生 +const pending = ref<'commit' | 'push' | 'commitPush' | null>(null) +const busy = computed(() => pending.value !== null) const generating = ref(false) // 文件视为已暂存:index 列非空且非未跟踪 @@ -155,29 +159,37 @@ const unstage = async (paths: string[]) => { const stageAll = () => stage(unstaged.value.map(f => f.path)) const unstageAll = () => unstage(staged.value.map(f => f.path)) +// 仅执行 git 调用,不管 pending(供组合动作复用) +const doCommit = async () => { + await invoke('git_commit', {root: props.rootDir, message: message.value.trim()}) + message.value = '' +} +const doPush = async () => { + await invoke('git_push', {root: props.rootDir}) +} + const commit = async () => { if (!canCommit.value) { return } - busy.value = true + pending.value = 'commit' try { - await invoke('git_commit', {root: props.rootDir, message: message.value.trim()}) + await doCommit() toast.success('已提交') - message.value = '' await refresh() } catch (error) { toast.error('提交失败: ' + error) } finally { - busy.value = false + pending.value = null } } const push = async () => { - busy.value = true + pending.value = 'push' try { - await invoke('git_push', {root: props.rootDir}) + await doPush() toast.success('已推送') await refresh() } @@ -185,7 +197,7 @@ const push = async () => { toast.error('推送失败: ' + error) } finally { - busy.value = false + pending.value = null } } @@ -194,8 +206,19 @@ const commitAndPush = async () => { toast.info('请填写提交信息并暂存改动') return } - await commit() - await push() + pending.value = 'commitPush' + try { + await doCommit() + await doPush() + toast.success('已提交并推送') + await refresh() + } + catch (error) { + toast.error('提交并推送失败: ' + error) + } + finally { + pending.value = null + } } const onBranchChange = async (e: Event) => { diff --git a/src/components/StatusBar.vue b/src/components/StatusBar.vue index ae03b36..6d41bce 100644 --- a/src/components/StatusBar.vue +++ b/src/components/StatusBar.vue @@ -22,13 +22,19 @@
- -
- - - {{ lspState.status === 'connecting' ? 'LSP 索引中' : 'LSP' }} -
+ +
@@ -46,10 +52,11 @@