feat: P2全部功能 - MCP协议+插件系统+任务管理+CLI增强
A组 CLI增强: - DiffRenderer: 彩色diff渲染(行号/stat摘要/unified格式) - /hooks: 查看已注册Hook列表 - /review: AI代码审查(支持--staged/文件路径) - /stats: 会话使用统计(tokens/费用/API调用/运行时长) - /branch: 对话分支管理(save/load/list/delete) - /rewind: 回退对话历史 - /tag: 对话位置标签 - /security-review: AI安全审查 B组 任务系统: - TaskManager: 后台任务管理(线程安全/自动执行/手动管理) - TaskCreate/TaskGet/TaskList/TaskUpdate 4个工具 - ConfigTool: 配置读写工具 C组 MCP协议: - McpClient: MCP客户端(JSON-RPC 2.0/工具发现/资源读取) - McpTransport: 传输层接口 - StdioTransport: StdIO传输实现(子进程/异步读写) - McpManager: 多MCP服务器管理(配置加载/生命周期) - McpToolBridge: MCP工具桥接为本地Tool - /mcp: MCP服务器管理命令 D组 插件系统: - Plugin接口 + PluginContext + PluginManager - JAR插件加载(ClassLoader隔离/Manifest读取) - OutputStylePlugin: 内置输出样式插件 - /plugin: 插件管理命令 集成: AppConfig注册全部18工具+28命令 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
package com.claudecode.console;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 差异视图渲染器 —— 对应 claude-code 中的 diff/ 组件。
|
||||
* 将 unified diff 格式的文本渲染为带颜色的终端输出。
|
||||
*/
|
||||
public class DiffRenderer {
|
||||
|
||||
private static final Pattern DIFF_HEADER = Pattern.compile("^diff --git a/(.*) b/(.*)$");
|
||||
private static final Pattern HUNK_HEADER = Pattern.compile("^@@ -(\\d+)(?:,(\\d+))? \\+(\\d+)(?:,(\\d+))? @@(.*)$");
|
||||
private static final Pattern STAT_LINE = Pattern.compile("^\\s*(.+?)\\s*\\|\\s*(\\d+)\\s*([+\\-]*)\\s*$");
|
||||
private static final Pattern BINARY_FILE = Pattern.compile("^Binary files .* differ$");
|
||||
private static final Pattern RENAME_FROM = Pattern.compile("^rename from (.+)$");
|
||||
private static final Pattern RENAME_TO = Pattern.compile("^rename to (.+)$");
|
||||
|
||||
/**
|
||||
* 渲染 unified diff 为彩色终端输出。
|
||||
*/
|
||||
public static String render(String diffText) {
|
||||
if (diffText == null || diffText.isBlank()) {
|
||||
return AnsiStyle.dim(" (no changes)");
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String line : diffText.split("\n")) {
|
||||
if (line.startsWith("diff --git")) {
|
||||
sb.append(AnsiStyle.bold(line)).append("\n");
|
||||
} else if (line.startsWith("---") || line.startsWith("+++")) {
|
||||
sb.append(AnsiStyle.bold(line)).append("\n");
|
||||
} else if (line.startsWith("@@")) {
|
||||
sb.append(AnsiStyle.cyan(line)).append("\n");
|
||||
} else if (line.startsWith("+")) {
|
||||
sb.append(AnsiStyle.green(line)).append("\n");
|
||||
} else if (line.startsWith("-")) {
|
||||
sb.append(AnsiStyle.red(line)).append("\n");
|
||||
} else if (line.startsWith("index ") || line.startsWith("new file") || line.startsWith("deleted file")) {
|
||||
sb.append(AnsiStyle.dim(line)).append("\n");
|
||||
} else if (BINARY_FILE.matcher(line).matches()) {
|
||||
sb.append(AnsiStyle.yellow(line)).append("\n");
|
||||
} else if (line.startsWith("rename ")) {
|
||||
sb.append(AnsiStyle.magenta(line)).append("\n");
|
||||
} else {
|
||||
sb.append(line).append("\n");
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染 diff 并附带行号。
|
||||
*/
|
||||
public static String renderWithLineNumbers(String diffText) {
|
||||
if (diffText == null || diffText.isBlank()) {
|
||||
return AnsiStyle.dim(" (no changes)");
|
||||
}
|
||||
|
||||
List<DiffFile> files = parse(diffText);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for (DiffFile file : files) {
|
||||
sb.append(AnsiStyle.bold("━━━ " + file.newPath() + " ━━━")).append("\n");
|
||||
|
||||
if (file.binary()) {
|
||||
sb.append(AnsiStyle.yellow(" Binary file")).append("\n\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
for (DiffHunk hunk : file.hunks()) {
|
||||
sb.append(AnsiStyle.cyan(hunk.header())).append("\n");
|
||||
|
||||
for (DiffLine dl : hunk.lines()) {
|
||||
String oldNum = dl.oldLineNum() > 0 ? String.format("%4d", dl.oldLineNum()) : " ";
|
||||
String newNum = dl.newLineNum() > 0 ? String.format("%4d", dl.newLineNum()) : " ";
|
||||
String lineNums = AnsiStyle.dim(oldNum + " " + newNum + " │ ");
|
||||
|
||||
switch (dl.type()) {
|
||||
case ADD -> sb.append(lineNums).append(AnsiStyle.green("+ " + dl.content())).append("\n");
|
||||
case REMOVE -> sb.append(lineNums).append(AnsiStyle.red("- " + dl.content())).append("\n");
|
||||
case CONTEXT -> sb.append(lineNums).append(" " + dl.content()).append("\n");
|
||||
case HEADER -> sb.append(AnsiStyle.cyan(dl.content())).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染 stat 摘要(类似 git diff --stat)。
|
||||
*/
|
||||
public static String renderStat(String diffText) {
|
||||
if (diffText == null || diffText.isBlank()) {
|
||||
return AnsiStyle.dim(" (no changes)");
|
||||
}
|
||||
|
||||
List<DiffFile> files = parse(diffText);
|
||||
if (files.isEmpty()) {
|
||||
return AnsiStyle.dim(" (no changes)");
|
||||
}
|
||||
|
||||
int totalAdded = 0, totalRemoved = 0;
|
||||
int maxNameLen = 0;
|
||||
List<FileStat> stats = new ArrayList<>();
|
||||
|
||||
for (DiffFile file : files) {
|
||||
int added = 0, removed = 0;
|
||||
for (DiffHunk hunk : file.hunks()) {
|
||||
for (DiffLine line : hunk.lines()) {
|
||||
if (line.type() == DiffLine.Type.ADD) added++;
|
||||
else if (line.type() == DiffLine.Type.REMOVE) removed++;
|
||||
}
|
||||
}
|
||||
String name = file.newPath();
|
||||
if (name.length() > maxNameLen) maxNameLen = name.length();
|
||||
stats.add(new FileStat(name, added, removed, file.binary()));
|
||||
totalAdded += added;
|
||||
totalRemoved += removed;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int barWidth = 40;
|
||||
|
||||
for (FileStat fs : stats) {
|
||||
String name = String.format(" %-" + (maxNameLen + 2) + "s", fs.name());
|
||||
|
||||
if (fs.binary()) {
|
||||
sb.append(name).append(AnsiStyle.dim(" | ")).append(AnsiStyle.yellow("Bin")).append("\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
int total = fs.added() + fs.removed();
|
||||
sb.append(name).append(AnsiStyle.dim(" | ")).append(String.format("%4d ", total));
|
||||
|
||||
// 条形图
|
||||
int maxBar = Math.min(total, barWidth);
|
||||
int addBar = total > 0 ? (int) Math.round((double) fs.added() / total * maxBar) : 0;
|
||||
int remBar = maxBar - addBar;
|
||||
sb.append(AnsiStyle.green("+".repeat(addBar)));
|
||||
sb.append(AnsiStyle.red("-".repeat(remBar)));
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
sb.append(AnsiStyle.dim(String.format(" %d file%s changed", files.size(), files.size() == 1 ? "" : "s")));
|
||||
if (totalAdded > 0) sb.append(AnsiStyle.green(String.format(", %d insertion%s(+)", totalAdded, totalAdded == 1 ? "" : "s")));
|
||||
if (totalRemoved > 0) sb.append(AnsiStyle.red(String.format(", %d deletion%s(-)", totalRemoved, totalRemoved == 1 ? "" : "s")));
|
||||
sb.append("\n");
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 unified diff 为结构化对象。
|
||||
*/
|
||||
public static List<DiffFile> parse(String diffText) {
|
||||
if (diffText == null || diffText.isBlank()) return List.of();
|
||||
|
||||
List<DiffFile> files = new ArrayList<>();
|
||||
String[] lines = diffText.split("\n");
|
||||
int i = 0;
|
||||
|
||||
while (i < lines.length) {
|
||||
Matcher headerMatch = DIFF_HEADER.matcher(lines[i]);
|
||||
if (!headerMatch.matches()) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
String oldPath = headerMatch.group(1);
|
||||
String newPath = headerMatch.group(2);
|
||||
i++;
|
||||
|
||||
// 跳过扩展头(index, mode, rename 等)
|
||||
boolean binary = false;
|
||||
while (i < lines.length && !lines[i].startsWith("---") && !lines[i].startsWith("diff --git") && !lines[i].startsWith("@@")) {
|
||||
if (BINARY_FILE.matcher(lines[i]).matches()) binary = true;
|
||||
if (lines[i].startsWith("rename to ")) {
|
||||
Matcher rm = RENAME_TO.matcher(lines[i]);
|
||||
if (rm.matches()) newPath = rm.group(1);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
if (binary) {
|
||||
files.add(new DiffFile(oldPath, newPath, List.of(), true));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 跳过 --- 和 +++ 行
|
||||
if (i < lines.length && lines[i].startsWith("---")) i++;
|
||||
if (i < lines.length && lines[i].startsWith("+++")) i++;
|
||||
|
||||
// 解析 hunks
|
||||
List<DiffHunk> hunks = new ArrayList<>();
|
||||
while (i < lines.length && !lines[i].startsWith("diff --git")) {
|
||||
Matcher hunkMatch = HUNK_HEADER.matcher(lines[i]);
|
||||
if (!hunkMatch.matches()) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
int oldStart = Integer.parseInt(hunkMatch.group(1));
|
||||
int oldCount = hunkMatch.group(2) != null ? Integer.parseInt(hunkMatch.group(2)) : 1;
|
||||
int newStart = Integer.parseInt(hunkMatch.group(3));
|
||||
int newCount = hunkMatch.group(4) != null ? Integer.parseInt(hunkMatch.group(4)) : 1;
|
||||
String hunkHeader = lines[i];
|
||||
i++;
|
||||
|
||||
List<DiffLine> diffLines = new ArrayList<>();
|
||||
int oldLine = oldStart;
|
||||
int newLine = newStart;
|
||||
|
||||
while (i < lines.length && !lines[i].startsWith("diff --git") && !lines[i].startsWith("@@")) {
|
||||
String line = lines[i];
|
||||
if (line.startsWith("+")) {
|
||||
diffLines.add(new DiffLine(DiffLine.Type.ADD, line.substring(1), -1, newLine++));
|
||||
} else if (line.startsWith("-")) {
|
||||
diffLines.add(new DiffLine(DiffLine.Type.REMOVE, line.substring(1), oldLine++, -1));
|
||||
} else if (line.startsWith(" ")) {
|
||||
diffLines.add(new DiffLine(DiffLine.Type.CONTEXT, line.substring(1), oldLine++, newLine++));
|
||||
} else if (line.startsWith("\\")) {
|
||||
// "\ No newline at end of file" — 跳过
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
hunks.add(new DiffHunk(oldStart, oldCount, newStart, newCount, hunkHeader, List.copyOf(diffLines)));
|
||||
}
|
||||
|
||||
files.add(new DiffFile(oldPath, newPath, List.copyOf(hunks), false));
|
||||
}
|
||||
|
||||
return List.copyOf(files);
|
||||
}
|
||||
|
||||
// ==================== 结构化类型 ====================
|
||||
|
||||
/** 差异文件 */
|
||||
public record DiffFile(String oldPath, String newPath, List<DiffHunk> hunks, boolean binary) {}
|
||||
|
||||
/** 差异区块 */
|
||||
public record DiffHunk(int oldStart, int oldCount, int newStart, int newCount, String header, List<DiffLine> lines) {}
|
||||
|
||||
/** 差异行 */
|
||||
public record DiffLine(Type type, String content, int oldLineNum, int newLineNum) {
|
||||
public enum Type { CONTEXT, ADD, REMOVE, HEADER }
|
||||
}
|
||||
|
||||
/** 文件统计(内部使用) */
|
||||
private record FileStat(String name, int added, int removed, boolean binary) {}
|
||||
}
|
||||
Reference in New Issue
Block a user