Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b51189a
chore(release): 版本号升至 26.3.0
qianmoQ Jun 18, 2026
0e9563b
feat(git): 新增提交 GPG 签名配置
qianmoQ Jun 18, 2026
e76a72d
feat(git): 新增 Git 钩子管理
qianmoQ Jun 18, 2026
ca62671
feat(editor): 支持文件内查找/替换 (Cmd/Ctrl+F)
qianmoQ Jun 18, 2026
4babbdb
style(editor): 美化文件内查找/替换面板
qianmoQ Jun 18, 2026
defd93b
feat(editor): 顶部工具栏新增 Git 面板常驻入口
qianmoQ Jun 18, 2026
cf62ff6
feat(editor): 支持代码折叠
qianmoQ Jun 18, 2026
6661f1e
fix(editor): 修复折叠箭头重复,去除与 basicSetup 重复的扩展
qianmoQ Jun 18, 2026
c513fb3
style(git): 头部图标改用 Tooltip 组件
qianmoQ Jun 18, 2026
055a68d
feat(ui): 新增全局 tooltip,统一所有原生 title 提示样式
qianmoQ Jun 18, 2026
b9289d7
feat(git): 面板支持拖拽改宽
qianmoQ Jun 18, 2026
ee55a80
feat(editor): 新增保存时格式化
qianmoQ Jun 18, 2026
f748515
feat(editor): 新增代码缩略图 (minimap)
qianmoQ Jun 18, 2026
5763b6c
fix(config): EditorConfig 增加 show_minimap 字段
qianmoQ Jun 18, 2026
de51445
style(editor): 优化代码缩略图显示
qianmoQ Jun 18, 2026
1f078f8
feat(git): 提交历史支持搜索与按作者过滤
qianmoQ Jun 18, 2026
eb099bf
feat(editor): 新增粘性滚动 (sticky scroll)
qianmoQ Jun 18, 2026
5ad644f
feat(run): 新增运行任务/构建预设 (B4)
qianmoQ Jun 18, 2026
9da02bf
feat(run): 支持发送选区到终端 (B3)
qianmoQ Jun 18, 2026
ae1f4b7
fix(run): 发送到终端应发送选区而非整行
qianmoQ Jun 18, 2026
193fca3
fix(editor): 右键点在选区内时保留选区高亮
qianmoQ Jun 18, 2026
ec4d0b5
feat(run): 新增运行测试(自动识别项目类型) (B2)
qianmoQ Jun 22, 2026
43db6dd
feat(debug): DAP 后端桥接 (B1-P0)
qianmoQ Jun 22, 2026
695ae2c
feat(debug): 前端 DAP 客户端 (B1-P1)
qianmoQ Jun 22, 2026
d774a76
feat(debug): 断点 gutter 与执行行高亮 (B1-P2)
qianmoQ Jun 22, 2026
232615d
feat(debug): 会话控制与调试工具栏 MVP (B1-P3)
qianmoQ Jun 22, 2026
45147c7
feat(debug): 调用栈与变量面板 (B1-P4)
qianmoQ Jun 22, 2026
1510425
feat(debug): 监视/求值/调试控制台与悬停求值 (B1-P5)
qianmoQ Jun 22, 2026
45f1053
feat(debug): 打通 Go(delve) 与适配器可用性提示 (B1-P6)
qianmoQ Jun 22, 2026
b5e393c
feat(debug): 设置页支持安装调试适配器 (B1-P6)
qianmoQ Jun 22, 2026
56ffabd
refactor(settings): 语言服务/调试适配器改用 Tabs 组件分页
qianmoQ Jun 22, 2026
8306c60
style(settings): 语言服务/调试适配器分页改用 card 样式
qianmoQ Jun 22, 2026
a0813bd
feat(debug): 断点列表面板与状态栏调试指示 (B1-P7)
qianmoQ Jun 22, 2026
37e8ebd
feat(debug): 支持条件断点 (B1-P7)
qianmoQ Jun 22, 2026
0be0e7a
feat(debug): 支持异常断点 (B1-P7)
qianmoQ Jun 22, 2026
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "codeforge",
"private": true,
"version": "26.2.0",
"version": "26.3.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down Expand Up @@ -32,6 +32,7 @@
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.38.1",
"@open-rpc/client-js": "^2.0.0",
"@replit/codemirror-minimap": "^0.5.2",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2.3.2",
"@tauri-apps/plugin-fs": "^2.4.2",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "CodeForge"
version = "26.2.0"
version = "26.3.0"
description = "CodeForge 是一款轻量级、高性能的桌面代码执行器,专为开发者、学生和编程爱好者设计。"
authors = ["devlive-community"]
edition = "2024"
Expand Down
8 changes: 8 additions & 0 deletions src-tauri/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub struct EditorConfig {
pub show_line_numbers: Option<bool>, // 是否显示行号
pub show_function_help: Option<bool>, // 是否显示函数帮助
pub space_dot_omission: Option<bool>, // 是否显示空格省略
pub show_minimap: Option<bool>, // 是否显示代码缩略图
pub show_sticky_scroll: Option<bool>, // 是否启用粘性滚动
pub layout: Option<String>, // 编辑器/控制台布局: horizontal | vertical | editor
pub last_direction: Option<String>, // 仅编辑器模式下控制台弹出方向: horizontal | vertical
pub max_open_file_size: Option<u32>, // 打开文件大小上限(MB),超过则拒绝打开
Expand Down Expand Up @@ -69,6 +71,8 @@ impl Default for AppConfig {
show_line_numbers: Some(true),
show_function_help: Some(false),
space_dot_omission: Some(false),
show_minimap: Some(false),
show_sticky_scroll: Some(false),
layout: Some("horizontal".to_string()),
last_direction: Some("horizontal".to_string()),
max_open_file_size: Some(5),
Expand Down Expand Up @@ -139,6 +143,8 @@ impl ConfigManager {
show_line_numbers: Some(true),
show_function_help: Some(false),
space_dot_omission: Some(false),
show_minimap: Some(false),
show_sticky_scroll: Some(false),
layout: Some("horizontal".to_string()),
last_direction: Some("horizontal".to_string()),
max_open_file_size: Some(5),
Expand Down Expand Up @@ -262,6 +268,8 @@ impl ConfigManager {
show_line_numbers: Some(true),
show_function_help: Some(false),
space_dot_omission: Some(false),
show_minimap: Some(false),
show_sticky_scroll: Some(false),
layout: Some("horizontal".to_string()),
last_direction: Some("horizontal".to_string()),
max_open_file_size: Some(5),
Expand Down
306 changes: 306 additions & 0 deletions src-tauri/src/dap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
//! DAP 桥接:按语言拉起调试适配器进程,转发 DAP 消息(与 LSP 同为 Content-Length 帧)。
//! 后端只做透明转发:前端发送/接收原始 JSON 字符串,握手与协议由前端的 DAP 客户端负责。
//! 复用 lsp.rs 的 find_in_path / augmented_path / read_message。

use crate::lsp::{augmented_path, find_in_path, read_message, read_raw_line};
use serde::Serialize;
use std::collections::HashMap;
use std::io::{BufReader, Read, Write};
use std::process::{Child, Command, Stdio};
use std::sync::Mutex as StdMutex;
use tauri::{AppHandle, Emitter, State};

struct Adapter {
child: Child,
// 写入走独立线程,避免 stdin 写阻塞主线程
tx: std::sync::mpsc::Sender<Vec<u8>>,
}

pub struct DapState {
// 以会话 key 区分(前端通常传语言名,单语言单会话)
adapters: StdMutex<HashMap<String, Adapter>>,
}

impl DapState {
pub fn new() -> Self {
Self {
adapters: StdMutex::new(HashMap::new()),
}
}
}

#[derive(Clone, Serialize)]
struct DapBatch {
session: String,
messages: Vec<String>,
}

/// 语言 -> (适配器可执行名, 启动参数)。适配器仅负责说 DAP;
/// 具体调试目标(program/args/cwd)由前端在 launch 请求中给出。新增语言在此加一行。
fn adapter_cmd(language: &str) -> Option<(&'static str, Vec<&'static str>)> {
match language {
// debugpy:python -m debugpy.adapter(需 pip install debugpy)
"python" | "python3" | "python2" => Some(("python3", vec!["-m", "debugpy.adapter"])),
// delve:dlv dap
"go" => Some(("dlv", vec!["dap"])),
// lldb-dap(LLVM 自带):适用于 Rust / C / C++
"rust" | "c" | "cpp" => Some(("lldb-dap", vec![])),
_ => None,
}
}

/// 该语言是否有可用的调试适配器。Python 进一步校验 debugpy 模块是否可导入。
#[tauri::command]
pub fn dap_available(language: String) -> bool {
let Some((prog, _)) = adapter_cmd(&language) else {
return false;
};
let Some(exe) = find_in_path(prog) else {
return false;
};
if matches!(language.as_str(), "python" | "python3" | "python2") {
return Command::new(&exe)
.args(["-c", "import debugpy"])
.env("PATH", augmented_path())
.output()
.map(|o| o.status.success())
.unwrap_or(false);
}
true
}

/// 启动调试适配器;已启动则直接返回 true。session 作为多会话区分键(前端传语言名即可)。
#[tauri::command]
pub fn dap_start(
app: AppHandle,
state: State<'_, DapState>,
session: String,
language: String,
) -> Result<bool, String> {
{
let adapters = state.adapters.lock().map_err(|e| e.to_string())?;
if adapters.contains_key(&session) {
return Ok(true);
}
}
let (prog, args) = adapter_cmd(&language).ok_or_else(|| "该语言暂不支持调试".to_string())?;
let exe = find_in_path(prog)
.ok_or_else(|| format!("未找到调试适配器:{}(请先安装并确保在 PATH 中)", prog))?;

let mut cmd = Command::new(&exe);
cmd.args(&args)
.env("PATH", augmented_path())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());

let mut child = cmd
.spawn()
.map_err(|e| format!("启动 {} 失败: {}", prog, e))?;
let stdin = child.stdin.take().ok_or("无法获取 stdin")?;
let stdout = child.stdout.take().ok_or("无法获取 stdout")?;
let stderr = child.stderr.take();

// 读取线程:解析 Content-Length 帧,丢进 channel
let (msg_tx, msg_rx) = std::sync::mpsc::channel::<String>();
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;
}
}
});

// 发射线程:合批转发(output/variables 等可能突发),会话结束发 dap:exit
let app_reader = app.clone();
let sess_emit = session.clone();
std::thread::spawn(move || {
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() >= 256 {
break;
}
}
let _ = app_reader.emit(
"dap:messages",
DapBatch {
session: sess_emit.clone(),
messages: batch,
},
);
}
let _ = app_reader.emit("dap:exit", sess_emit.clone());
});

// 排空 stderr,避免阻塞
if let Some(mut err) = stderr {
std::thread::spawn(move || {
let mut buf = [0u8; 4096];
while let Ok(n) = err.read(&mut buf) {
if n == 0 {
break;
}
}
});
}

// 写入线程:独占 stdin,从 channel 取帧写入
let (tx, rx) = std::sync::mpsc::channel::<Vec<u8>>();
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
.adapters
.lock()
.map_err(|e| e.to_string())?
.insert(session, Adapter { child, tx });
Ok(true)
}

/// 向调试适配器发送一条 DAP 消息(已是完整 JSON 字符串)
#[tauri::command]
pub fn dap_send(
state: State<'_, DapState>,
session: String,
message: String,
) -> Result<(), String> {
let adapters = state.adapters.lock().map_err(|e| e.to_string())?;
let adapter = adapters
.get(&session)
.ok_or_else(|| "调试会话未启动".to_string())?;
let frame = format!("Content-Length: {}\r\n\r\n{}", message.len(), message);
adapter
.tx
.send(frame.into_bytes())
.map_err(|_| "调试适配器写入通道已关闭".to_string())?;
Ok(())
}

/// 停止调试会话
#[tauri::command]
pub fn dap_stop(state: State<'_, DapState>, session: String) -> Result<(), String> {
if let Some(mut adapter) = state
.adapters
.lock()
.map_err(|e| e.to_string())?
.remove(&session)
{
let _ = adapter.child.kill();
}
Ok(())
}

// 可安装的调试适配器:(id, 展示名, 安装命令)
fn adapter_defs() -> Vec<(&'static str, &'static str, &'static str)> {
vec![
(
"debugpy",
"Python (debugpy)",
"python3 -m pip install debugpy",
),
(
"delve",
"Go (delve)",
"go install github.com/go-delve/delve/cmd/dlv@latest",
),
]
}

fn adapter_installed(id: &str) -> bool {
match id {
// debugpy 校验模块可导入
"debugpy" => dap_available("python".to_string()),
"delve" => find_in_path("dlv").is_some(),
_ => false,
}
}

#[derive(Serialize)]
pub struct DapAdapterInfo {
id: String,
label: String,
installed: bool,
install: String,
}

/// 列出可安装的调试适配器及其安装状态
#[tauri::command]
pub fn dap_adapter_list() -> Vec<DapAdapterInfo> {
adapter_defs()
.into_iter()
.map(|(id, label, install)| DapAdapterInfo {
id: id.to_string(),
label: label.to_string(),
installed: adapter_installed(id),
install: install.to_string(),
})
.collect()
}

/// 一键安装某调试适配器:执行安装命令并实时输出日志(事件 dap:install / dap:install-done)
#[tauri::command]
pub fn dap_install(app: AppHandle, id: String) -> Result<(), String> {
let def = adapter_defs()
.into_iter()
.find(|(d, ..)| *d == id)
.ok_or_else(|| "未知的调试适配器".to_string())?;
let cmd_str = def.2.to_string();

let (shell, flag) = if cfg!(windows) {
("cmd", "/C")
} else {
("sh", "-c")
};
let mut child = Command::new(shell)
.arg(flag)
.arg(&cmd_str)
.env("PATH", augmented_path())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("无法执行安装命令: {}", e))?;

let stdout = child.stdout.take();
let stderr = child.stderr.take();
let id_done = id.clone();
let app_done = app.clone();

let emit_lines = |app: AppHandle, id: String, reader: Option<Box<dyn Read + Send>>| {
if let Some(r) = reader {
std::thread::spawn(move || {
let mut buf = BufReader::new(r);
while let Some(line) = read_raw_line(&mut buf) {
let _ = app.emit("dap:install", (id.clone(), line));
}
});
}
};
emit_lines(
app.clone(),
id.clone(),
stdout.map(|s| Box::new(s) as Box<dyn Read + Send>),
);
emit_lines(
app.clone(),
id.clone(),
stderr.map(|s| Box::new(s) as Box<dyn Read + Send>),
);

std::thread::spawn(move || {
let success = child.wait().map(|s| s.success()).unwrap_or(false);
let _ = app_done.emit("dap:install-done", (id_done, success));
});
Ok(())
}
Loading
Loading