diff --git a/package.json b/package.json index f9e0ed6..466b55f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "codeforge", "private": true, - "version": "26.4.0", + "version": "26.5.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ba99ea0..12dd2b3 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "CodeForge" -version = "26.4.0" +version = "26.5.0" dependencies = [ "async-trait", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3c263cd..7396e42 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "CodeForge" -version = "26.4.0" +version = "26.5.0" description = "CodeForge 是一款轻量级、高性能的桌面代码执行器,专为开发者、学生和编程爱好者设计。" authors = ["devlive-community"] edition = "2024" diff --git a/src-tauri/src/filesystem.rs b/src-tauri/src/filesystem.rs index d165366..0a758ed 100644 --- a/src-tauri/src/filesystem.rs +++ b/src-tauri/src/filesystem.rs @@ -1328,6 +1328,18 @@ pub async fn git_merge(root: String, branch: String) -> Result { .map_err(|e| format!("git 任务失败: {}", e))? } +/// 冲突文件整文件取一侧:side = "ours" | "theirs",取该侧内容并暂存(标记已解决)。 +#[tauri::command] +pub async fn git_resolve_side(root: String, path: String, side: String) -> Result { + let flag = if side == "theirs" { "--theirs" } else { "--ours" }; + tokio::task::spawn_blocking(move || { + run_git(&root, &["checkout", flag, "--", &path])?; + run_git(&root, &["add", "--", &path]) + }) + .await + .map_err(|e| format!("git 任务失败: {}", e))? +} + #[derive(Serialize)] pub struct GitCommit { hash: String, diff --git a/src-tauri/src/github.rs b/src-tauri/src/github.rs new file mode 100644 index 0000000..3b1b6e3 --- /dev/null +++ b/src-tauri/src/github.rs @@ -0,0 +1,241 @@ +//! GitHub REST API 集成:用用户配置的 token 直接在应用内创建/查看 PR 与 Issue。 +use serde::Serialize; +use serde_json::{Value, json}; + +const API: &str = "https://api.github.com"; + +#[derive(Serialize)] +pub struct PrItem { + number: u64, + title: String, + url: String, + state: String, + head: String, + base: String, + author: String, + draft: bool, +} + +#[derive(Serialize)] +pub struct IssueItem { + number: u64, + title: String, + url: String, + state: String, + author: String, + comments: u64, +} + +fn client() -> reqwest::Client { + reqwest::Client::new() +} + +// 统一的鉴权 GET +async fn api_get(token: &str, path: &str) -> Result { + let resp = client() + .get(format!("{}{}", API, path)) + .header("Authorization", format!("Bearer {}", token)) + .header("Accept", "application/vnd.github+json") + .header("User-Agent", "CodeForge") + .header("X-GitHub-Api-Version", "2022-11-28") + .send() + .await + .map_err(|e| format!("请求失败: {}", e))?; + let status = resp.status(); + let data: Value = resp + .json() + .await + .map_err(|e| format!("解析响应失败: {}", e))?; + if !status.is_success() { + return Err(extract_error(&data, status.as_u16())); + } + Ok(data) +} + +// 统一的鉴权 POST +async fn api_post(token: &str, path: &str, body: Value) -> Result { + let resp = client() + .post(format!("{}{}", API, path)) + .header("Authorization", format!("Bearer {}", token)) + .header("Accept", "application/vnd.github+json") + .header("User-Agent", "CodeForge") + .header("X-GitHub-Api-Version", "2022-11-28") + .json(&body) + .send() + .await + .map_err(|e| format!("请求失败: {}", e))?; + let status = resp.status(); + let data: Value = resp + .json() + .await + .map_err(|e| format!("解析响应失败: {}", e))?; + if !status.is_success() { + return Err(extract_error(&data, status.as_u16())); + } + Ok(data) +} + +fn extract_error(data: &Value, status: u16) -> String { + let msg = data["message"].as_str().unwrap_or("未知错误"); + // 附带首个字段级错误,便于定位 + let detail = data["errors"][0]["message"] + .as_str() + .map(|s| format!("({})", s)) + .unwrap_or_default(); + format!("GitHub API 错误 ({}): {}{}", status, msg, detail) +} + +fn pr_from(v: &Value) -> PrItem { + PrItem { + number: v["number"].as_u64().unwrap_or(0), + title: v["title"].as_str().unwrap_or("").to_string(), + url: v["html_url"].as_str().unwrap_or("").to_string(), + state: v["state"].as_str().unwrap_or("").to_string(), + head: v["head"]["ref"].as_str().unwrap_or("").to_string(), + base: v["base"]["ref"].as_str().unwrap_or("").to_string(), + author: v["user"]["login"].as_str().unwrap_or("").to_string(), + draft: v["draft"].as_bool().unwrap_or(false), + } +} + +fn issue_from(v: &Value) -> IssueItem { + IssueItem { + number: v["number"].as_u64().unwrap_or(0), + title: v["title"].as_str().unwrap_or("").to_string(), + url: v["html_url"].as_str().unwrap_or("").to_string(), + state: v["state"].as_str().unwrap_or("").to_string(), + author: v["user"]["login"].as_str().unwrap_or("").to_string(), + comments: v["comments"].as_u64().unwrap_or(0), + } +} + +fn require_token(token: &str) -> Result<(), String> { + if token.trim().is_empty() { + return Err("未配置 GitHub Token(设置 → 通用 → GitHub)".to_string()); + } + Ok(()) +} + +/// 列出仓库的 Pull Request(默认 open)。 +#[tauri::command] +pub async fn github_list_prs( + token: String, + owner: String, + repo: String, + state: Option, +) -> Result, String> { + require_token(&token)?; + let st = state.unwrap_or_else(|| "open".to_string()); + let data = api_get( + &token, + &format!("/repos/{}/{}/pulls?state={}&per_page=50", owner, repo, st), + ) + .await?; + Ok(data + .as_array() + .map(|a| a.iter().map(pr_from).collect()) + .unwrap_or_default()) +} + +/// 创建 Pull Request。 +#[tauri::command] +pub async fn github_create_pr( + token: String, + owner: String, + repo: String, + title: String, + head: String, + base: String, + body: Option, + draft: Option, +) -> Result { + require_token(&token)?; + let payload = json!({ + "title": title, + "head": head, + "base": base, + "body": body.unwrap_or_default(), + "draft": draft.unwrap_or(false), + }); + let data = api_post(&token, &format!("/repos/{}/{}/pulls", owner, repo), payload).await?; + Ok(pr_from(&data)) +} + +/// 列出仓库的 Issue(默认 open;GitHub 会把 PR 也算作 issue,这里过滤掉)。 +#[tauri::command] +pub async fn github_list_issues( + token: String, + owner: String, + repo: String, + state: Option, +) -> Result, String> { + require_token(&token)?; + let st = state.unwrap_or_else(|| "open".to_string()); + let data = api_get( + &token, + &format!("/repos/{}/{}/issues?state={}&per_page=50", owner, repo, st), + ) + .await?; + Ok(data + .as_array() + .map(|a| { + a.iter() + .filter(|v| v.get("pull_request").is_none()) + .map(issue_from) + .collect() + }) + .unwrap_or_default()) +} + +#[derive(Serialize)] +pub struct RepoBranches { + default_branch: String, + branches: Vec, +} + +/// 取仓库的默认分支与全部分支(供新建 PR 选择 base)。 +#[tauri::command] +pub async fn github_repo_branches( + token: String, + owner: String, + repo: String, +) -> Result { + require_token(&token)?; + let info = api_get(&token, &format!("/repos/{}/{}", owner, repo)).await?; + let default_branch = info["default_branch"] + .as_str() + .unwrap_or("main") + .to_string(); + let brs = api_get( + &token, + &format!("/repos/{}/{}/branches?per_page=100", owner, repo), + ) + .await?; + let branches = brs + .as_array() + .map(|a| { + a.iter() + .filter_map(|b| b["name"].as_str().map(String::from)) + .collect() + }) + .unwrap_or_default(); + Ok(RepoBranches { + default_branch, + branches, + }) +} + +/// 创建 Issue。 +#[tauri::command] +pub async fn github_create_issue( + token: String, + owner: String, + repo: String, + title: String, + body: Option, +) -> Result { + require_token(&token)?; + let payload = json!({ "title": title, "body": body.unwrap_or_default() }); + let data = api_post(&token, &format!("/repos/{}/{}/issues", owner, repo), payload).await?; + Ok(issue_from(&data)) +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 1445a61..b9f2d62 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -16,6 +16,7 @@ mod env_manager; mod env_providers; mod example; mod execution; +mod github; mod filesystem; mod font; mod geo; @@ -71,7 +72,7 @@ use crate::filesystem::{ git_file_diff, git_file_head, git_get_identity, git_get_signing, git_graph, git_hook_delete, git_hook_read, git_hook_save, git_hooks, git_ignore_add, git_ignore_append_block, git_init, git_log, git_log_file, git_merge, git_op_abort, git_op_continue, git_op_skip, git_op_state, - git_permalink, git_pull, git_pull_rebase, git_push, git_push_force, git_push_tags, + git_permalink, git_pull, git_pull_rebase, git_push, git_push_force, git_push_tags, git_resolve_side, git_rebase_interactive, git_reflog, git_remote_add, git_remote_branches, git_remote_remove, git_remotes, git_reset, git_restore_file, git_revert, git_set_identity, git_set_signing, git_set_upstream, git_show, git_stage, git_staged_diff, git_stash_apply, git_stash_drop, @@ -256,6 +257,12 @@ fn main() { git_remote_branches, git_checkout_track, git_merge, + git_resolve_side, + github::github_list_prs, + github::github_create_pr, + github::github_repo_branches, + github::github_list_issues, + github::github_create_issue, git_blame, git_file_head, git_log, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index da7bb97..b38589d 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.4.0", + "version": "26.5.0", "identifier": "org.devlive.codeforge", "build": { "beforeDevCommand": "pnpm dev", diff --git a/src/App.vue b/src/App.vue index d7224db..69bac80 100644 --- a/src/App.vue +++ b/src/App.vue @@ -427,6 +427,9 @@ + + + @@ -434,9 +437,11 @@ - @@ -539,7 +544,7 @@ import {debounce} from 'lodash-es' import {formatDocument, formatSelection, renameSymbol} from 'codemirror-languageserver' import {runGotoDefinition, lspSupportsLanguage, triggerCodeActions, applyCodeAction, formatDocumentAsync} from './editor/lspExtension' import {dapSupportsLanguage} from './debug/dapClient' -import {ArrowDownAZ, ArrowUpAZ, Bookmark, CaseLower, CaseUpper, ChevronRight, Code2, CornerDownRight, Eraser, Eye, FoldVertical, FolderOpen, GitBranch, GitCompare, History, ListChecks, ListTree, Maximize2, Minimize2, Monitor, Moon, PanelBottom, PanelLeft, PanelRight, Play, Plus, Save, Search, Settings as SettingsIcon, Sparkles, Sun, Terminal as TerminalIcon, UnfoldVertical, WrapText, X} from 'lucide-vue-next' +import {ArrowDownAZ, ArrowUpAZ, Bookmark, CaseLower, CaseUpper, ChevronRight, Code2, CornerDownRight, Eraser, Eye, FoldVertical, FolderOpen, GitBranch, GitCompare, History, Image as ImageIcon, ListChecks, ListTree, Maximize2, Minimize2, Monitor, Moon, PanelBottom, PanelLeft, PanelRight, Play, Plus, Save, Search, Settings as SettingsIcon, Sparkles, Sun, Terminal as TerminalIcon, UnfoldVertical, WrapText, 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' @@ -603,6 +608,7 @@ import DebugToolbar from './components/DebugToolbar.vue' import DebugPanel from './components/DebugPanel.vue' import AiCodeAction from './components/AiCodeAction.vue' import AiMultiEdit from './components/AiMultiEdit.vue' +import CodeImageExport from './components/CodeImageExport.vue' import ErDiagram from './components/ErDiagram.vue' import TxnControl from './components/TxnControl.vue' import {useSqlTxn} from './composables/useSqlTxn' @@ -786,9 +792,17 @@ const rootDir = ref(null) // 远程仓库永久链接(复制 / 在浏览器打开) const {copyPermalink, openPermalink, openRepoOnWeb} = useGitPermalink(rootDir, currentFilePath, cursorInfo) -// 多根工作区:额外挂载的文件夹(Git/搜索仍走主根 rootDir) +// 多根工作区:额外挂载的文件夹 const {extraRoots, addWorkspaceFolder, removeWorkspaceFolder, resetExtraRoots, setExtraRoots} = useWorkspaceRoots(rootDir) +// 活动根:Git 面板针对哪个根(各根独立 Git);无效时回退主根 +const activeGitRoot = ref(null) +const allRoots = computed(() => [rootDir.value, ...extraRoots.value].filter((r): r is string => !!r)) +const gitRoot = computed(() => { + const a = activeGitRoot.value + return a && allRoots.value.includes(a) ? a : rootDir.value +}) + // 命名工作区:保存/打开一组根 const showWorkspaces = ref(false) const openWorkspace = (ws: {rootDir: string; extraRoots: string[]}) => { @@ -966,6 +980,17 @@ const selectedOrAll = (): string => { return code.value } +// 导出代码为图片(选中片段或整篇) +const codeImageCtx = ref<{ code: string } | null>(null) +const openCodeImage = () => { + const src = selectedOrAll() + if (!src.trim()) { + toast.info(t('app.noCodeToImage')) + return + } + codeImageCtx.value = {code: src} +} + // 以一次性提示打开 AI(临时会话,不关联执行) const openAiWithPrompt = (prompt: string) => { aiExecutionId.value = null @@ -2553,6 +2578,7 @@ const paletteCommands = computed(() => [ {id: 'removeDuplicateLines', label: t('command.removeDuplicateLines'), group: t('command.groupText'), icon: ListChecks, run: () => removeDuplicateLines()}, {id: 'trimTrailingWhitespace', label: t('command.trimTrailingWhitespace'), group: t('command.groupText'), icon: Eraser, run: () => trimTrailingWhitespace()}, {id: 'copyAsMarkdown', label: t('command.copyAsMarkdown'), group: t('command.groupText'), icon: Code2, run: () => copyAsMarkdown()}, + {id: 'exportCodeImage', label: t('command.exportCodeImage'), group: t('command.groupText'), icon: ImageIcon, run: openCodeImage}, {id: 'indentToSpaces', label: t('command.indentToSpaces'), group: t('command.groupText'), icon: Eraser, run: () => convertIndentation(false)}, {id: 'indentToTabs', label: t('command.indentToTabs'), group: t('command.groupText'), icon: Eraser, run: () => convertIndentation(true)}, {id: 'foldAll', label: t('command.foldAll'), group: t('command.groupCode'), icon: FoldVertical, run: () => { if (editorView.value) foldAll(editorView.value) }}, diff --git a/src/components/CodeImageExport.vue b/src/components/CodeImageExport.vue new file mode 100644 index 0000000..30e7d6c --- /dev/null +++ b/src/components/CodeImageExport.vue @@ -0,0 +1,197 @@ + + + diff --git a/src/components/ConflictResolver.vue b/src/components/ConflictResolver.vue new file mode 100644 index 0000000..f2110f9 --- /dev/null +++ b/src/components/ConflictResolver.vue @@ -0,0 +1,196 @@ + + + diff --git a/src/components/GitHubPanel.vue b/src/components/GitHubPanel.vue new file mode 100644 index 0000000..6982331 --- /dev/null +++ b/src/components/GitHubPanel.vue @@ -0,0 +1,203 @@ + + + diff --git a/src/components/GitPanel.vue b/src/components/GitPanel.vue index 2224e34..63a44bf 100644 --- a/src/components/GitPanel.vue +++ b/src/components/GitPanel.vue @@ -7,6 +7,14 @@
+ +