diff --git a/README.md b/README.md index ba0117c..b0e712d 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,125 @@
- + -

CodeForge

+

CodeForge

-CodeForge 是一款轻量级、高性能的桌面代码执行器,专为开发者、学生和编程爱好者设计。 +**一款现代化的桌面端多语言代码编辑器与运行器** + +集「文件/项目编辑 · 一键运行 · AI 助手 · Git 集成 · 集成终端 · 结构化数据可视化」于一身, +让你在一个轻量优雅的桌面应用里完成编写、运行、调试与协作。 + +

+ version + platform + stack + license +

-## 演示视频 - -📹 [下载演示视频](https://devlive-cdn.oss-cn-beijing.aliyuncs.com/applications/codeforge/codeforge.mp4) (点击下载或观看) - -> 注:由于 GitHub 不支持直接播放视频,请下载或点击链接查看 - -## 特性 - -- 🚀 **即时执行** - 一键运行代码 -- 💻 **现代界面** - 简洁优雅的用户体验 -- 📝 **智能编辑** - 代码高亮、行号、自动缩进 -- 📊 **执行监控** - 实时显示执行时间和结果 -- 🔧 **插件架构** - 可扩展的语言支持系统 - -## 支持的语言 - -
- C - Cangjie - Clojure - C++ - CSS - Go - Groovy - Haskell - HTML - Java - JavaScript (Browser) - JavaScript (jQuery) - JavaScript (Node.js) - Kotlin - Lua - Node.js - Objective-C - Objective-C++ - PHP - Python 2 - Python 3 - R - Ruby - Rust - Shell - SVG - Swift - TypeScript - TypeScript (Browser) - TypeScript (Node.js) +--- + +## 📹 演示视频 + +[下载演示视频](https://devlive-cdn.oss-cn-beijing.aliyuncs.com/applications/codeforge/codeforge.mp4)(点击下载或观看) + +> GitHub 不支持直接播放视频,请下载或点击链接查看。 + +--- + +## ✨ 核心功能 + +### 📂 编辑与项目 +- **文件树侧栏 + 多标签编辑** —— 打开文件夹,像 IDE 一样浏览、编辑整个项目 +- **面包屑路径导航** —— 点击任意层级在系统文件管理器中定位 +- **命令面板**(`Cmd/Ctrl + Shift + P`)—— 一处入口直达所有命令 +- **快速打开**(`Cmd/Ctrl + P`)—— 模糊匹配 + 最近文件优先 +- **符号大纲**(`Cmd/Ctrl + Shift + O`)、**跳转到行**(`Cmd/Ctrl + G`) +- **代码片段** —— 自定义前缀,输入后按 `Tab` 展开(`$0` 为光标落点) +- **会话恢复** —— 重启自动恢复上次的文件夹与标签页 +- **深色模式** —— 跟随系统 / 浅色 / 深色,编辑器主题同步切换 + +### ▶️ 运行与调试 +- **一键运行 / 按文件就地运行**,实时流式输出、执行耗时统计 +- **运行选中片段**(`Cmd/Ctrl + Shift + Enter`) +- **监听模式** —— 保存后自动重跑 +- **运行输入** —— 自定义参数 / stdin / 环境变量,并按文件记忆 +- **执行历史** —— 持久化保存,可一键重跑与还原 + +### 📊 结构化数据可视化 +- **JSON / XML / YAML** —— 可折叠**层级树**,以及卡片 + 连线的**关系图**两种可视化 +- **Markdown** —— 实时渲染预览(支持内嵌 HTML,DOMPurify 净化防 XSS) +- **GitHub Actions 工作流** —— 自动识别并渲染为 **Jobs 依赖 DAG 图**(触发事件 → 各 Job → Steps) + +### 🤖 AI 助手 +- **多服务商** —— Claude (Anthropic) / OpenAI / DeepSeek +- **AI 代码预测** —— 编辑器内幽灵补全,`Tab` 接受 +- **解释代码 / 生成测试 / 格式化代码** —— 一键发起,应用前可 diff 预览确认 +- **报错分析、自然语言生成代码、生成 Git 提交信息** +- **对话与执行历史绑定** —— 每次执行的 AI 讨论可追溯 + +### 🔱 Git 集成 +- **源代码管理面板** —— 暂存 / 提交 / 推送 / 分支切换,AI 一键生成提交信息 +- **文件树状态徽标**(M / A / D / U) +- **编辑器行内差异标记** —— 相对 HEAD 的增 / 改 / 删 + +### 🔎 搜索与终端 +- **文件夹内搜索与替换**(`Cmd/Ctrl + Shift + F`) +- **集成终端** —— 真实 shell、多标签、可拖拽改高度(`` Cmd/Ctrl + ` ``) + +--- + +## 🧩 支持的语言 + +可运行语言均采用**插件化架构**,每种语言独立实现;JSON / XML / YAML / Markdown / 纯文本为编辑与可视化类型。 + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
-## 安装 +
-**系统要求:** +`Python` · `Node.js` · `TypeScript` · `JavaScript` · `Go` · `Rust` · `Java` · `Kotlin` · `Scala` · `Groovy` · `Clojure` · `C` · `C++` · `Objective-C/C++` · `Swift` · `Ruby` · `PHP` · `R` · `Lua` · `Haskell` · `Cangjie` · `Shell` · `AppleScript` · `HTML` · `CSS` · `SVG` · `JSON` · `XML` · `YAML` · `Markdown` · `Text` -- Node.js 18+ -- Rust 1.8+ -- Tauri 2.x -- Vue 3.x +
-**构建步骤:** +--- + +## 🚀 安装与构建 + +**环境要求:** Node.js 22+ · Rust 1.8+ · pnpm ```bash # 克隆项目 @@ -81,12 +136,23 @@ pnpm tauri dev pnpm tauri build ``` -## 技术栈 +--- + +## 🛠 技术栈 + +| 层 | 技术 | +| --- | --- | +| 前端 | Vue 3 · TypeScript · Tailwind CSS · CodeMirror 6 | +| 后端 | Rust · Tauri 2 | +| 存储 | SQLite(执行历史 / AI 对话 / 代码片段 / 应用配置统一入库) | +| 架构 | 插件化语言支持系统 | + +--- + +## 🤝 贡献与反馈 -- **前端:** Vue 3 + TypeScript + Tailwind CSS -- **后端:** Rust + Tauri -- **架构:** 插件化语言支持系统 +欢迎提交 Issue 与 PR: -## 许可证 +## 📄 许可证 -MIT License \ No newline at end of file +[MIT License](LICENSE) diff --git a/package.json b/package.json index 1f6fa75..50e2f35 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@codemirror/lang-php": "^6.0.2", "@codemirror/lang-python": "^6.2.1", "@codemirror/lang-rust": "^6.0.2", + "@codemirror/lang-sql": "^6.10.0", "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.3", "@codemirror/language": "^6.11.2", @@ -38,6 +39,8 @@ "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "codemirror": "^6.0.2", + "dompurify": "^3.4.8", + "js-yaml": "^4.2.0", "lodash-es": "^4.17.21", "lucide-vue-next": "^0.539.0", "markdown-it": "^14.2.0", @@ -48,6 +51,7 @@ "devDependencies": { "@tailwindcss/postcss": "^4.1.11", "@tauri-apps/cli": "^2", + "@types/js-yaml": "^4.0.9", "@types/lodash-es": "^4.17.12", "@types/markdown-it": "^14.1.2", "@vitejs/plugin-vue": "^5.2.1", diff --git a/public/icons/sql.svg b/public/icons/sql.svg new file mode 100644 index 0000000..a10961d --- /dev/null +++ b/public/icons/sql.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src-tauri/src/ai_history.rs b/src-tauri/src/ai_history.rs index 916c209..8aa0d08 100644 --- a/src-tauri/src/ai_history.rs +++ b/src-tauri/src/ai_history.rs @@ -14,6 +14,8 @@ impl AiHistory { let db_path = get_codeforge_db_path()?; let conn = Connection::open(&db_path).map_err(|e| format!("打开数据库失败: {}", e))?; let _ = conn.pragma_update(None, "journal_mode", "WAL"); + let _ = conn.pragma_update(None, "synchronous", "NORMAL"); + let _ = conn.busy_timeout(std::time::Duration::from_secs(5)); // 清理早期错误结构的旧表 let _ = conn.execute("DROP TABLE IF EXISTS ai_conversations", []); conn.execute( diff --git a/src-tauri/src/execution.rs b/src-tauri/src/execution.rs index bf72c6b..761d6cf 100644 --- a/src-tauri/src/execution.rs +++ b/src-tauri/src/execution.rs @@ -41,6 +41,12 @@ impl ExecutionHistory { let conn = Connection::open(&db_path).map_err(|e| format!("打开执行历史数据库失败: {}", e))?; + // 与其它连接(ai/kv/snippets)共用同一个库文件:统一 WAL + NORMAL + busy_timeout, + // 避免并发写入(如运行时同时有 kv 写入)相互阻塞导致 insert 卡顿 + let _ = conn.pragma_update(None, "journal_mode", "WAL"); + let _ = conn.pragma_update(None, "synchronous", "NORMAL"); + let _ = conn.busy_timeout(std::time::Duration::from_secs(5)); + conn.execute( "CREATE TABLE IF NOT EXISTS execution_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -627,7 +633,7 @@ pub async fn execute_code( return Ok(result); } Ok(None) => { - tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + tokio::time::sleep(tokio::time::Duration::from_millis(20)).await; } Err(e) => { let _ = child.kill(); diff --git a/src-tauri/src/kv.rs b/src-tauri/src/kv.rs index a0bf793..d4eab09 100644 --- a/src-tauri/src/kv.rs +++ b/src-tauri/src/kv.rs @@ -15,6 +15,8 @@ impl KvStore { let db_path = get_codeforge_db_path()?; let conn = Connection::open(&db_path).map_err(|e| format!("打开数据库失败: {}", e))?; let _ = conn.pragma_update(None, "journal_mode", "WAL"); + let _ = conn.pragma_update(None, "synchronous", "NORMAL"); + let _ = conn.busy_timeout(std::time::Duration::from_secs(5)); conn.execute( "CREATE TABLE IF NOT EXISTS kv_store ( key TEXT PRIMARY KEY, diff --git a/src-tauri/src/plugins/json.rs b/src-tauri/src/plugins/json.rs index e228d3a..37ff321 100644 --- a/src-tauri/src/plugins/json.rs +++ b/src-tauri/src/plugins/json.rs @@ -37,11 +37,12 @@ impl LanguagePlugin for JsonPlugin { before_compile: None, extension: String::from("json"), execute_home: None, + // 输出原始 JSON 内容,由前端 JSON 视图(可折叠树)渲染 run_command: Some(String::from("cat $filename")), after_compile: None, template: Some(String::from("{\n \n}")), timeout: Some(30), - console_type: Some(String::from("console")), + console_type: Some(String::from("json")), icon_path: None, } } diff --git a/src-tauri/src/plugins/manager.rs b/src-tauri/src/plugins/manager.rs index 374d7d9..587883c 100644 --- a/src-tauri/src/plugins/manager.rs +++ b/src-tauri/src/plugins/manager.rs @@ -29,6 +29,7 @@ use crate::plugins::ruby::RubyPlugin; use crate::plugins::rust::RustPlugin; use crate::plugins::scala::ScalaPlugin; use crate::plugins::shell::ShellPlugin; +use crate::plugins::sql::SqlPlugin; use crate::plugins::svg::SvgPlugin; use crate::plugins::swift::SwiftPlugin; use crate::plugins::text::TextPlugin; @@ -75,6 +76,7 @@ impl PluginManager { ("yaml".to_string(), Box::new(YamlPlugin)), ("markdown".to_string(), Box::new(MarkdownPlugin)), ("text".to_string(), Box::new(TextPlugin)), + ("sql".to_string(), Box::new(SqlPlugin)), ("php".to_string(), Box::new(PHPPlugin)), ("r".to_string(), Box::new(RPlugin)), ("cangjie".to_string(), Box::new(CangjiePlugin)), diff --git a/src-tauri/src/plugins/markdown.rs b/src-tauri/src/plugins/markdown.rs index 6336c8c..89e6fa6 100644 --- a/src-tauri/src/plugins/markdown.rs +++ b/src-tauri/src/plugins/markdown.rs @@ -37,11 +37,12 @@ impl LanguagePlugin for MarkdownPlugin { before_compile: None, extension: String::from("md"), execute_home: None, + // 输出原始 Markdown,由前端渲染为预览 run_command: Some(String::from("cat $filename")), after_compile: None, template: Some(String::from("# 标题\n\n在这里输入 Markdown 内容\n")), timeout: Some(30), - console_type: Some(String::from("console")), + console_type: Some(String::from("markdown")), icon_path: None, } } diff --git a/src-tauri/src/plugins/mod.rs b/src-tauri/src/plugins/mod.rs index eed10ee..c3a86a6 100644 --- a/src-tauri/src/plugins/mod.rs +++ b/src-tauri/src/plugins/mod.rs @@ -429,6 +429,7 @@ pub mod ruby; pub mod rust; pub mod scala; pub mod shell; +pub mod sql; pub mod svg; pub mod swift; pub mod text; diff --git a/src-tauri/src/plugins/sql.rs b/src-tauri/src/plugins/sql.rs new file mode 100644 index 0000000..bd353d3 --- /dev/null +++ b/src-tauri/src/plugins/sql.rs @@ -0,0 +1,57 @@ +use super::{LanguagePlugin, PluginConfig}; +use std::vec; + +pub struct SqlPlugin; + +impl LanguagePlugin for SqlPlugin { + fn get_order(&self) -> i32 { + 28 + } + + fn get_language_name(&self) -> &'static str { + "SQL" + } + + fn get_language_key(&self) -> &'static str { + "sql" + } + + fn get_file_extension(&self) -> String { + self.get_config() + .map(|config| config.extension.clone()) + .unwrap_or_else(|| "sql".to_string()) + } + + fn get_version_args(&self) -> Vec<&'static str> { + vec!["--version"] + } + + fn get_path_command(&self) -> String { + "sqlite3".to_string() + } + + fn get_default_config(&self) -> PluginConfig { + PluginConfig { + enabled: true, + language: String::from("sql"), + before_compile: None, + extension: String::from("sql"), + execute_home: None, + // 在内存 SQLite 中执行脚本(需要本机有 sqlite3) + run_command: Some(String::from("sqlite3 :memory: .read $filename")), + after_compile: None, + template: Some(String::from( + "-- 在这里输入 SQL(默认在内存 SQLite 中执行)\nSELECT 'Hello, CodeForge' AS message;\n", + )), + timeout: Some(30), + console_type: Some(String::from("console")), + icon_path: None, + } + } + + fn get_default_command(&self) -> String { + self.get_config() + .and_then(|config| config.run_command.clone()) + .unwrap_or_else(|| "sqlite3".to_string()) + } +} diff --git a/src-tauri/src/plugins/xml.rs b/src-tauri/src/plugins/xml.rs index 31e4eeb..a93ac0c 100644 --- a/src-tauri/src/plugins/xml.rs +++ b/src-tauri/src/plugins/xml.rs @@ -37,11 +37,12 @@ impl LanguagePlugin for XmlPlugin { before_compile: None, extension: String::from("xml"), execute_home: None, + // 输出原始 XML,由前端 XML 视图(可折叠树)渲染 run_command: Some(String::from("cat $filename")), after_compile: None, template: Some(String::from("\n")), timeout: Some(30), - console_type: Some(String::from("console")), + console_type: Some(String::from("xml")), icon_path: None, } } diff --git a/src-tauri/src/plugins/yaml.rs b/src-tauri/src/plugins/yaml.rs index bd04999..417c5c7 100644 --- a/src-tauri/src/plugins/yaml.rs +++ b/src-tauri/src/plugins/yaml.rs @@ -46,11 +46,12 @@ impl LanguagePlugin for YamlPlugin { before_compile: None, extension: String::from("yaml,yml"), execute_home: None, + // 输出原始 YAML,由前端 YAML 视图(可折叠树)渲染 run_command: Some(String::from("cat $filename")), after_compile: None, template: Some(String::from("# 在这里输入 YAML 内容\n")), timeout: Some(30), - console_type: Some(String::from("console")), + console_type: Some(String::from("yaml")), icon_path: None, } } diff --git a/src-tauri/src/snippets.rs b/src-tauri/src/snippets.rs index 8056db6..27df8b8 100644 --- a/src-tauri/src/snippets.rs +++ b/src-tauri/src/snippets.rs @@ -26,6 +26,8 @@ impl Snippets { let db_path = get_codeforge_db_path()?; let conn = Connection::open(&db_path).map_err(|e| format!("打开数据库失败: {}", e))?; let _ = conn.pragma_update(None, "journal_mode", "WAL"); + let _ = conn.pragma_update(None, "synchronous", "NORMAL"); + let _ = conn.busy_timeout(std::time::Duration::from_secs(5)); conn.execute( "CREATE TABLE IF NOT EXISTS snippets ( id TEXT PRIMARY KEY, diff --git a/src/App.vue b/src/App.vue index d60189a..02d6127 100644 --- a/src/App.vue +++ b/src/App.vue @@ -142,6 +142,38 @@ :execution-time="lastExecutionTime" @clear="clearOutput"> + + + + + + + + + + + +
@@ -200,7 +232,7 @@ @close="showTerminal = false; terminalMounted = false"/> - + @@ -311,6 +343,10 @@ import AppHeader from './components/AppHeader.vue' import CodeEditor from './components/CodeEditor.vue' import ConsoleOutput from './components/ConsoleOutput.vue' import WebOutput from "./components/WebOutput.vue"; +import JsonView from "./components/JsonView.vue"; +import MarkdownView from "./components/MarkdownView.vue"; +import XmlView from "./components/XmlView.vue"; +import YamlView from "./components/YamlView.vue"; import StatusBar from './components/StatusBar.vue' import About from './components/About.vue' import Settings from './components/Settings.vue' @@ -1121,6 +1157,11 @@ const { const editorConfigKey = ref(0) const consoleType = ref('console') +// 语言变化(打开文件/切换标签/下拉切换)时同步输出类型,否则 JSON/Markdown 等视图不会激活 +watch(currentLanguage, () => { + consoleType.value = getCurrentConsoleType() +}) + // ===== 布局管理 ===== // 当前布局模式:horizontal(左右) / vertical(上下) / editor(仅编辑器) const layoutMode = computed(() => editorConfig.value?.layout || 'horizontal') diff --git a/src/components/DataGraph.vue b/src/components/DataGraph.vue new file mode 100644 index 0000000..cf03329 --- /dev/null +++ b/src/components/DataGraph.vue @@ -0,0 +1,149 @@ + + + diff --git a/src/components/JsonNode.vue b/src/components/JsonNode.vue new file mode 100644 index 0000000..a1394c3 --- /dev/null +++ b/src/components/JsonNode.vue @@ -0,0 +1,88 @@ + + + diff --git a/src/components/JsonView.vue b/src/components/JsonView.vue new file mode 100644 index 0000000..7d1297d --- /dev/null +++ b/src/components/JsonView.vue @@ -0,0 +1,100 @@ + + + diff --git a/src/components/MarkdownView.vue b/src/components/MarkdownView.vue new file mode 100644 index 0000000..fe753b0 --- /dev/null +++ b/src/components/MarkdownView.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/src/components/PreviewPanel.vue b/src/components/PreviewPanel.vue index 75c1b3a..f5da0d4 100644 --- a/src/components/PreviewPanel.vue +++ b/src/components/PreviewPanel.vue @@ -33,6 +33,7 @@ import {computed} from 'vue' import {Eye, X} from 'lucide-vue-next' import MarkdownIt from 'markdown-it' +import DOMPurify from 'dompurify' const props = defineProps<{ content: string @@ -41,7 +42,8 @@ const props = defineProps<{ }>() const emit = defineEmits<{ close: [] }>() -const md = new MarkdownIt({html: false, linkify: true, breaks: true}) +// 允许 Markdown 内的 HTML,渲染后用 DOMPurify 净化防 XSS +const md = new MarkdownIt({html: true, linkify: true, breaks: true}) // 根据语言或扩展名判断预览类型 const kind = computed<'markdown' | 'html' | 'none'>(() => { @@ -58,7 +60,7 @@ const kind = computed<'markdown' | 'html' | 'none'>(() => { const kindLabel = computed(() => ({markdown: 'Markdown', html: 'HTML', none: '不支持'}[kind.value])) -const rendered = computed(() => (kind.value === 'markdown' ? md.render(props.content || '') : '')) +const rendered = computed(() => (kind.value === 'markdown' ? DOMPurify.sanitize(md.render(props.content || '')) : ''))