jarvis-ai-assistant 0.7.8__py3-none-any.whl → 1.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +567 -222
  3. jarvis/jarvis_agent/agent_manager.py +19 -12
  4. jarvis/jarvis_agent/builtin_input_handler.py +79 -11
  5. jarvis/jarvis_agent/config_editor.py +7 -2
  6. jarvis/jarvis_agent/event_bus.py +24 -13
  7. jarvis/jarvis_agent/events.py +19 -1
  8. jarvis/jarvis_agent/file_context_handler.py +67 -64
  9. jarvis/jarvis_agent/file_methodology_manager.py +38 -24
  10. jarvis/jarvis_agent/jarvis.py +186 -114
  11. jarvis/jarvis_agent/language_extractors/__init__.py +8 -1
  12. jarvis/jarvis_agent/language_extractors/c_extractor.py +7 -4
  13. jarvis/jarvis_agent/language_extractors/cpp_extractor.py +9 -4
  14. jarvis/jarvis_agent/language_extractors/go_extractor.py +7 -4
  15. jarvis/jarvis_agent/language_extractors/java_extractor.py +27 -20
  16. jarvis/jarvis_agent/language_extractors/javascript_extractor.py +22 -17
  17. jarvis/jarvis_agent/language_extractors/python_extractor.py +7 -4
  18. jarvis/jarvis_agent/language_extractors/rust_extractor.py +7 -4
  19. jarvis/jarvis_agent/language_extractors/typescript_extractor.py +22 -17
  20. jarvis/jarvis_agent/language_support_info.py +250 -219
  21. jarvis/jarvis_agent/main.py +19 -23
  22. jarvis/jarvis_agent/memory_manager.py +9 -6
  23. jarvis/jarvis_agent/methodology_share_manager.py +21 -15
  24. jarvis/jarvis_agent/output_handler.py +4 -2
  25. jarvis/jarvis_agent/prompt_builder.py +7 -6
  26. jarvis/jarvis_agent/prompt_manager.py +113 -8
  27. jarvis/jarvis_agent/prompts.py +317 -85
  28. jarvis/jarvis_agent/protocols.py +5 -2
  29. jarvis/jarvis_agent/run_loop.py +192 -32
  30. jarvis/jarvis_agent/session_manager.py +7 -3
  31. jarvis/jarvis_agent/share_manager.py +23 -13
  32. jarvis/jarvis_agent/shell_input_handler.py +12 -8
  33. jarvis/jarvis_agent/stdio_redirect.py +25 -26
  34. jarvis/jarvis_agent/task_analyzer.py +29 -23
  35. jarvis/jarvis_agent/task_list.py +869 -0
  36. jarvis/jarvis_agent/task_manager.py +26 -23
  37. jarvis/jarvis_agent/tool_executor.py +6 -5
  38. jarvis/jarvis_agent/tool_share_manager.py +24 -14
  39. jarvis/jarvis_agent/user_interaction.py +3 -3
  40. jarvis/jarvis_agent/utils.py +9 -1
  41. jarvis/jarvis_agent/web_bridge.py +37 -17
  42. jarvis/jarvis_agent/web_output_sink.py +5 -2
  43. jarvis/jarvis_agent/web_server.py +165 -36
  44. jarvis/jarvis_c2rust/__init__.py +1 -1
  45. jarvis/jarvis_c2rust/cli.py +260 -141
  46. jarvis/jarvis_c2rust/collector.py +37 -18
  47. jarvis/jarvis_c2rust/constants.py +60 -0
  48. jarvis/jarvis_c2rust/library_replacer.py +242 -1010
  49. jarvis/jarvis_c2rust/library_replacer_checkpoint.py +133 -0
  50. jarvis/jarvis_c2rust/library_replacer_llm.py +287 -0
  51. jarvis/jarvis_c2rust/library_replacer_loader.py +191 -0
  52. jarvis/jarvis_c2rust/library_replacer_output.py +134 -0
  53. jarvis/jarvis_c2rust/library_replacer_prompts.py +124 -0
  54. jarvis/jarvis_c2rust/library_replacer_utils.py +188 -0
  55. jarvis/jarvis_c2rust/llm_module_agent.py +98 -1044
  56. jarvis/jarvis_c2rust/llm_module_agent_apply.py +170 -0
  57. jarvis/jarvis_c2rust/llm_module_agent_executor.py +288 -0
  58. jarvis/jarvis_c2rust/llm_module_agent_loader.py +170 -0
  59. jarvis/jarvis_c2rust/llm_module_agent_prompts.py +268 -0
  60. jarvis/jarvis_c2rust/llm_module_agent_types.py +57 -0
  61. jarvis/jarvis_c2rust/llm_module_agent_utils.py +150 -0
  62. jarvis/jarvis_c2rust/llm_module_agent_validator.py +119 -0
  63. jarvis/jarvis_c2rust/loaders.py +28 -10
  64. jarvis/jarvis_c2rust/models.py +5 -2
  65. jarvis/jarvis_c2rust/optimizer.py +192 -1974
  66. jarvis/jarvis_c2rust/optimizer_build_fix.py +286 -0
  67. jarvis/jarvis_c2rust/optimizer_clippy.py +766 -0
  68. jarvis/jarvis_c2rust/optimizer_config.py +49 -0
  69. jarvis/jarvis_c2rust/optimizer_docs.py +183 -0
  70. jarvis/jarvis_c2rust/optimizer_options.py +48 -0
  71. jarvis/jarvis_c2rust/optimizer_progress.py +469 -0
  72. jarvis/jarvis_c2rust/optimizer_report.py +52 -0
  73. jarvis/jarvis_c2rust/optimizer_unsafe.py +309 -0
  74. jarvis/jarvis_c2rust/optimizer_utils.py +469 -0
  75. jarvis/jarvis_c2rust/optimizer_visibility.py +185 -0
  76. jarvis/jarvis_c2rust/scanner.py +229 -166
  77. jarvis/jarvis_c2rust/transpiler.py +531 -2732
  78. jarvis/jarvis_c2rust/transpiler_agents.py +503 -0
  79. jarvis/jarvis_c2rust/transpiler_build.py +1294 -0
  80. jarvis/jarvis_c2rust/transpiler_codegen.py +204 -0
  81. jarvis/jarvis_c2rust/transpiler_compile.py +146 -0
  82. jarvis/jarvis_c2rust/transpiler_config.py +178 -0
  83. jarvis/jarvis_c2rust/transpiler_context.py +122 -0
  84. jarvis/jarvis_c2rust/transpiler_executor.py +516 -0
  85. jarvis/jarvis_c2rust/transpiler_generation.py +278 -0
  86. jarvis/jarvis_c2rust/transpiler_git.py +163 -0
  87. jarvis/jarvis_c2rust/transpiler_mod_utils.py +225 -0
  88. jarvis/jarvis_c2rust/transpiler_modules.py +336 -0
  89. jarvis/jarvis_c2rust/transpiler_planning.py +394 -0
  90. jarvis/jarvis_c2rust/transpiler_review.py +1196 -0
  91. jarvis/jarvis_c2rust/transpiler_symbols.py +176 -0
  92. jarvis/jarvis_c2rust/utils.py +269 -79
  93. jarvis/jarvis_code_agent/after_change.py +233 -0
  94. jarvis/jarvis_code_agent/build_validation_config.py +37 -30
  95. jarvis/jarvis_code_agent/builtin_rules.py +68 -0
  96. jarvis/jarvis_code_agent/code_agent.py +976 -1517
  97. jarvis/jarvis_code_agent/code_agent_build.py +227 -0
  98. jarvis/jarvis_code_agent/code_agent_diff.py +246 -0
  99. jarvis/jarvis_code_agent/code_agent_git.py +525 -0
  100. jarvis/jarvis_code_agent/code_agent_impact.py +177 -0
  101. jarvis/jarvis_code_agent/code_agent_lint.py +283 -0
  102. jarvis/jarvis_code_agent/code_agent_llm.py +159 -0
  103. jarvis/jarvis_code_agent/code_agent_postprocess.py +105 -0
  104. jarvis/jarvis_code_agent/code_agent_prompts.py +46 -0
  105. jarvis/jarvis_code_agent/code_agent_rules.py +305 -0
  106. jarvis/jarvis_code_agent/code_analyzer/__init__.py +52 -48
  107. jarvis/jarvis_code_agent/code_analyzer/base_language.py +12 -10
  108. jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +12 -11
  109. jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +16 -12
  110. jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +26 -17
  111. jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +558 -104
  112. jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +27 -16
  113. jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +22 -18
  114. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +21 -16
  115. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +20 -16
  116. jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +27 -16
  117. jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +47 -23
  118. jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +71 -37
  119. jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +162 -35
  120. jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +111 -57
  121. jarvis/jarvis_code_agent/code_analyzer/build_validator.py +18 -12
  122. jarvis/jarvis_code_agent/code_analyzer/context_manager.py +185 -183
  123. jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +2 -1
  124. jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +24 -15
  125. jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +227 -141
  126. jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +321 -247
  127. jarvis/jarvis_code_agent/code_analyzer/language_registry.py +37 -29
  128. jarvis/jarvis_code_agent/code_analyzer/language_support.py +21 -13
  129. jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +15 -9
  130. jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +75 -45
  131. jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +87 -52
  132. jarvis/jarvis_code_agent/code_analyzer/languages/java_language.py +84 -51
  133. jarvis/jarvis_code_agent/code_analyzer/languages/javascript_language.py +94 -64
  134. jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +109 -71
  135. jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +97 -63
  136. jarvis/jarvis_code_agent/code_analyzer/languages/typescript_language.py +103 -69
  137. jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +271 -268
  138. jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +76 -64
  139. jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +92 -19
  140. jarvis/jarvis_code_agent/diff_visualizer.py +998 -0
  141. jarvis/jarvis_code_agent/lint.py +223 -524
  142. jarvis/jarvis_code_agent/rule_share_manager.py +158 -0
  143. jarvis/jarvis_code_agent/rules/clean_code.md +144 -0
  144. jarvis/jarvis_code_agent/rules/code_review.md +115 -0
  145. jarvis/jarvis_code_agent/rules/documentation.md +165 -0
  146. jarvis/jarvis_code_agent/rules/generate_rules.md +52 -0
  147. jarvis/jarvis_code_agent/rules/performance.md +158 -0
  148. jarvis/jarvis_code_agent/rules/refactoring.md +139 -0
  149. jarvis/jarvis_code_agent/rules/security.md +160 -0
  150. jarvis/jarvis_code_agent/rules/tdd.md +78 -0
  151. jarvis/jarvis_code_agent/test_rules/cpp_test.md +118 -0
  152. jarvis/jarvis_code_agent/test_rules/go_test.md +98 -0
  153. jarvis/jarvis_code_agent/test_rules/java_test.md +99 -0
  154. jarvis/jarvis_code_agent/test_rules/javascript_test.md +113 -0
  155. jarvis/jarvis_code_agent/test_rules/php_test.md +117 -0
  156. jarvis/jarvis_code_agent/test_rules/python_test.md +91 -0
  157. jarvis/jarvis_code_agent/test_rules/ruby_test.md +102 -0
  158. jarvis/jarvis_code_agent/test_rules/rust_test.md +86 -0
  159. jarvis/jarvis_code_agent/utils.py +36 -26
  160. jarvis/jarvis_code_analysis/checklists/loader.py +21 -21
  161. jarvis/jarvis_code_analysis/code_review.py +64 -33
  162. jarvis/jarvis_data/config_schema.json +285 -192
  163. jarvis/jarvis_git_squash/main.py +8 -6
  164. jarvis/jarvis_git_utils/git_commiter.py +53 -76
  165. jarvis/jarvis_mcp/__init__.py +5 -2
  166. jarvis/jarvis_mcp/sse_mcp_client.py +40 -30
  167. jarvis/jarvis_mcp/stdio_mcp_client.py +27 -19
  168. jarvis/jarvis_mcp/streamable_mcp_client.py +35 -26
  169. jarvis/jarvis_memory_organizer/memory_organizer.py +78 -55
  170. jarvis/jarvis_methodology/main.py +48 -39
  171. jarvis/jarvis_multi_agent/__init__.py +56 -23
  172. jarvis/jarvis_multi_agent/main.py +15 -18
  173. jarvis/jarvis_platform/base.py +179 -111
  174. jarvis/jarvis_platform/human.py +27 -16
  175. jarvis/jarvis_platform/kimi.py +52 -45
  176. jarvis/jarvis_platform/openai.py +101 -40
  177. jarvis/jarvis_platform/registry.py +51 -33
  178. jarvis/jarvis_platform/tongyi.py +68 -38
  179. jarvis/jarvis_platform/yuanbao.py +59 -43
  180. jarvis/jarvis_platform_manager/main.py +68 -76
  181. jarvis/jarvis_platform_manager/service.py +24 -14
  182. jarvis/jarvis_rag/README_CONFIG.md +314 -0
  183. jarvis/jarvis_rag/README_DYNAMIC_LOADING.md +311 -0
  184. jarvis/jarvis_rag/README_ONLINE_MODELS.md +230 -0
  185. jarvis/jarvis_rag/__init__.py +57 -4
  186. jarvis/jarvis_rag/cache.py +3 -1
  187. jarvis/jarvis_rag/cli.py +48 -68
  188. jarvis/jarvis_rag/embedding_interface.py +39 -0
  189. jarvis/jarvis_rag/embedding_manager.py +7 -230
  190. jarvis/jarvis_rag/embeddings/__init__.py +41 -0
  191. jarvis/jarvis_rag/embeddings/base.py +114 -0
  192. jarvis/jarvis_rag/embeddings/cohere.py +66 -0
  193. jarvis/jarvis_rag/embeddings/edgefn.py +117 -0
  194. jarvis/jarvis_rag/embeddings/local.py +260 -0
  195. jarvis/jarvis_rag/embeddings/openai.py +62 -0
  196. jarvis/jarvis_rag/embeddings/registry.py +293 -0
  197. jarvis/jarvis_rag/llm_interface.py +8 -6
  198. jarvis/jarvis_rag/query_rewriter.py +8 -9
  199. jarvis/jarvis_rag/rag_pipeline.py +61 -52
  200. jarvis/jarvis_rag/reranker.py +7 -75
  201. jarvis/jarvis_rag/reranker_interface.py +32 -0
  202. jarvis/jarvis_rag/rerankers/__init__.py +41 -0
  203. jarvis/jarvis_rag/rerankers/base.py +109 -0
  204. jarvis/jarvis_rag/rerankers/cohere.py +67 -0
  205. jarvis/jarvis_rag/rerankers/edgefn.py +140 -0
  206. jarvis/jarvis_rag/rerankers/jina.py +79 -0
  207. jarvis/jarvis_rag/rerankers/local.py +89 -0
  208. jarvis/jarvis_rag/rerankers/registry.py +293 -0
  209. jarvis/jarvis_rag/retriever.py +58 -43
  210. jarvis/jarvis_sec/__init__.py +66 -141
  211. jarvis/jarvis_sec/agents.py +21 -17
  212. jarvis/jarvis_sec/analysis.py +80 -33
  213. jarvis/jarvis_sec/checkers/__init__.py +7 -13
  214. jarvis/jarvis_sec/checkers/c_checker.py +356 -164
  215. jarvis/jarvis_sec/checkers/rust_checker.py +47 -29
  216. jarvis/jarvis_sec/cli.py +43 -21
  217. jarvis/jarvis_sec/clustering.py +430 -272
  218. jarvis/jarvis_sec/file_manager.py +99 -55
  219. jarvis/jarvis_sec/parsers.py +9 -6
  220. jarvis/jarvis_sec/prompts.py +4 -3
  221. jarvis/jarvis_sec/report.py +44 -22
  222. jarvis/jarvis_sec/review.py +180 -107
  223. jarvis/jarvis_sec/status.py +50 -41
  224. jarvis/jarvis_sec/types.py +3 -0
  225. jarvis/jarvis_sec/utils.py +160 -83
  226. jarvis/jarvis_sec/verification.py +411 -181
  227. jarvis/jarvis_sec/workflow.py +132 -21
  228. jarvis/jarvis_smart_shell/main.py +28 -41
  229. jarvis/jarvis_stats/cli.py +14 -12
  230. jarvis/jarvis_stats/stats.py +28 -19
  231. jarvis/jarvis_stats/storage.py +14 -8
  232. jarvis/jarvis_stats/visualizer.py +12 -7
  233. jarvis/jarvis_tools/base.py +5 -2
  234. jarvis/jarvis_tools/clear_memory.py +13 -9
  235. jarvis/jarvis_tools/cli/main.py +23 -18
  236. jarvis/jarvis_tools/edit_file.py +572 -873
  237. jarvis/jarvis_tools/execute_script.py +10 -7
  238. jarvis/jarvis_tools/file_analyzer.py +7 -8
  239. jarvis/jarvis_tools/meta_agent.py +287 -0
  240. jarvis/jarvis_tools/methodology.py +5 -3
  241. jarvis/jarvis_tools/read_code.py +305 -1438
  242. jarvis/jarvis_tools/read_symbols.py +50 -17
  243. jarvis/jarvis_tools/read_webpage.py +19 -18
  244. jarvis/jarvis_tools/registry.py +435 -156
  245. jarvis/jarvis_tools/retrieve_memory.py +16 -11
  246. jarvis/jarvis_tools/save_memory.py +8 -6
  247. jarvis/jarvis_tools/search_web.py +31 -31
  248. jarvis/jarvis_tools/sub_agent.py +32 -28
  249. jarvis/jarvis_tools/sub_code_agent.py +44 -60
  250. jarvis/jarvis_tools/task_list_manager.py +1811 -0
  251. jarvis/jarvis_tools/virtual_tty.py +29 -19
  252. jarvis/jarvis_utils/__init__.py +4 -0
  253. jarvis/jarvis_utils/builtin_replace_map.py +2 -1
  254. jarvis/jarvis_utils/clipboard.py +9 -8
  255. jarvis/jarvis_utils/collections.py +331 -0
  256. jarvis/jarvis_utils/config.py +699 -194
  257. jarvis/jarvis_utils/dialogue_recorder.py +294 -0
  258. jarvis/jarvis_utils/embedding.py +6 -3
  259. jarvis/jarvis_utils/file_processors.py +7 -1
  260. jarvis/jarvis_utils/fzf.py +9 -3
  261. jarvis/jarvis_utils/git_utils.py +71 -42
  262. jarvis/jarvis_utils/globals.py +116 -32
  263. jarvis/jarvis_utils/http.py +6 -2
  264. jarvis/jarvis_utils/input.py +318 -83
  265. jarvis/jarvis_utils/jsonnet_compat.py +119 -104
  266. jarvis/jarvis_utils/methodology.py +37 -28
  267. jarvis/jarvis_utils/output.py +201 -44
  268. jarvis/jarvis_utils/utils.py +986 -628
  269. {jarvis_ai_assistant-0.7.8.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/METADATA +49 -33
  270. jarvis_ai_assistant-1.0.2.dist-info/RECORD +304 -0
  271. jarvis/jarvis_code_agent/code_analyzer/structured_code.py +0 -556
  272. jarvis/jarvis_tools/generate_new_tool.py +0 -205
  273. jarvis/jarvis_tools/lsp_client.py +0 -1552
  274. jarvis/jarvis_tools/rewrite_file.py +0 -105
  275. jarvis_ai_assistant-0.7.8.dist-info/RECORD +0 -218
  276. {jarvis_ai_assistant-0.7.8.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/WHEEL +0 -0
  277. {jarvis_ai_assistant-0.7.8.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/entry_points.txt +0 -0
  278. {jarvis_ai_assistant-0.7.8.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/licenses/LICENSE +0 -0
  279. {jarvis_ai_assistant-0.7.8.dist-info → jarvis_ai_assistant-1.0.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,998 @@
1
+ # -*- coding: utf-8 -*-
2
+ """改进的 Diff 可视化工具
3
+
4
+ 提供多种 diff 可视化方式,改善代码变更的可读性。
5
+ """
6
+
7
+ import difflib
8
+ from typing import List
9
+ from typing import Optional
10
+ from typing import Union
11
+
12
+ from rich.console import Console
13
+ from rich.panel import Panel
14
+ from rich.syntax import Syntax
15
+ from rich.table import Table
16
+ from rich.text import Text
17
+
18
+ LANGUAGE_EXTENSION_MAPPING = {
19
+ "py": "python",
20
+ "js": "javascript",
21
+ "ts": "typescript",
22
+ "java": "java",
23
+ "cpp": "cpp",
24
+ "cc": "cpp",
25
+ "cxx": "cpp",
26
+ "c": "c",
27
+ "h": "c",
28
+ "hpp": "cpp",
29
+ "rs": "rust",
30
+ "go": "go",
31
+ "sh": "bash",
32
+ "bash": "bash",
33
+ "zsh": "zsh",
34
+ "html": "html",
35
+ "css": "css",
36
+ "json": "json",
37
+ "yaml": "yaml",
38
+ "yml": "yaml",
39
+ "xml": "xml",
40
+ "md": "markdown",
41
+ "markdown": "markdown",
42
+ "toml": "toml",
43
+ "dockerfile": "dockerfile",
44
+ "ps1": "powershell",
45
+ "ini": "ini",
46
+ "make": "makefile",
47
+ "mk": "makefile",
48
+ }
49
+
50
+
51
+ class DiffVisualizer:
52
+ """改进的 Diff 可视化工具"""
53
+
54
+ def __init__(self, console: Optional[Console] = None):
55
+ """初始化可视化器
56
+
57
+ 参数:
58
+ console: Rich Console 实例,如果为 None 则创建新实例
59
+ """
60
+ self.console = console or Console()
61
+
62
+ def _get_language_by_extension(self, file_path: str) -> str:
63
+ """根据文件扩展名推断编程语言
64
+
65
+ 参数:
66
+ file_path: 文件路径
67
+
68
+ 返回:
69
+ 编程语言名称(用于 Syntax 高亮)
70
+ """
71
+ if not file_path:
72
+ return "text"
73
+
74
+ ext = file_path.lower().split(".")[-1] if "." in file_path else ""
75
+ return LANGUAGE_EXTENSION_MAPPING.get(ext, "text")
76
+
77
+ def visualize_unified_diff(
78
+ self,
79
+ diff_text: str,
80
+ file_path: str = "",
81
+ show_line_numbers: bool = True,
82
+ context_lines: int = 3,
83
+ ) -> None:
84
+ """可视化统一格式的 diff(改进版)
85
+
86
+ 参数:
87
+ diff_text: git diff 输出的文本
88
+ file_path: 文件路径(用于显示标题)
89
+ show_line_numbers: 是否显示行号
90
+ context_lines: 上下文行数
91
+ """
92
+ if not diff_text.strip():
93
+ return
94
+
95
+ lines = diff_text.split("\n")
96
+
97
+ # 创建表格显示
98
+ table = Table(
99
+ show_header=False,
100
+ header_style="bold magenta",
101
+ box=None, # 无边框,更简洁
102
+ padding=(0, 1),
103
+ )
104
+
105
+ if show_line_numbers:
106
+ table.add_column("", style="red", width=8, justify="right")
107
+ table.add_column("", style="green", width=8, justify="right")
108
+ table.add_column("", width=4, justify="center")
109
+ table.add_column("", style="white", overflow="fold")
110
+
111
+ old_line_num = 0
112
+ new_line_num = 0
113
+ in_hunk = False
114
+ hunk_lines: list = [] # 存储当前 hunk 中的所有行(包括上下文和变更)
115
+
116
+ def flush_hunk_context():
117
+ """刷新当前 hunk,只显示 context_lines 数量的上下文"""
118
+ nonlocal hunk_lines
119
+ if not hunk_lines:
120
+ return
121
+
122
+ # 找到所有变更行的索引
123
+ change_indices = []
124
+ for idx, (line_type, _, _, _) in enumerate(hunk_lines):
125
+ if line_type in ("-", "+"):
126
+ change_indices.append(idx)
127
+
128
+ if not change_indices:
129
+ # 没有变更,只显示最后 context_lines 行上下文
130
+ for line_type, old_ln, new_ln, content in hunk_lines[-context_lines:]:
131
+ if show_line_numbers:
132
+ table.add_row(
133
+ str(old_ln),
134
+ str(new_ln),
135
+ " ",
136
+ f"{content}",
137
+ )
138
+ else:
139
+ table.add_row("", " ", f"{content}")
140
+ else:
141
+ # 有变更,只显示变更前后的上下文
142
+ first_change_idx = change_indices[0]
143
+ last_change_idx = change_indices[-1]
144
+
145
+ # 显示变更前的上下文(最多 context_lines 行)
146
+ pre_context_start = max(0, first_change_idx - context_lines)
147
+ for idx in range(pre_context_start, first_change_idx):
148
+ _, old_ln, new_ln, content = hunk_lines[idx]
149
+ if show_line_numbers:
150
+ table.add_row(
151
+ str(old_ln),
152
+ str(new_ln),
153
+ " ",
154
+ f"{content}",
155
+ )
156
+ else:
157
+ table.add_row("", " ", f"{content}")
158
+
159
+ # 显示所有变更行
160
+ for idx in range(first_change_idx, last_change_idx + 1):
161
+ line_type, old_ln, new_ln, content = hunk_lines[idx]
162
+ if line_type == "-":
163
+ if show_line_numbers:
164
+ table.add_row(
165
+ str(old_ln) if old_ln else "",
166
+ "",
167
+ "[bold red]-[/bold red]",
168
+ f"[bright_red]{content}[/bright_red]",
169
+ )
170
+ else:
171
+ table.add_row(
172
+ "",
173
+ "[bold red]-[/bold red]",
174
+ f"[bright_red]{content}[/bright_red]",
175
+ )
176
+ elif line_type == "+":
177
+ if show_line_numbers:
178
+ table.add_row(
179
+ "",
180
+ str(new_ln) if new_ln else "",
181
+ "[bold green]+[/bold green]",
182
+ f"[bright_green]{content}[/bright_green]",
183
+ )
184
+ else:
185
+ table.add_row(
186
+ "",
187
+ "[bold green]+[/bold green]",
188
+ f"[bright_green]{content}[/bright_green]",
189
+ )
190
+
191
+ # 显示变更后的上下文(最多 context_lines 行)
192
+ post_context_end = min(
193
+ len(hunk_lines), last_change_idx + 1 + context_lines
194
+ )
195
+ for idx in range(last_change_idx + 1, post_context_end):
196
+ _, old_ln, new_ln, content = hunk_lines[idx]
197
+ if show_line_numbers:
198
+ table.add_row(
199
+ str(old_ln),
200
+ str(new_ln),
201
+ " ",
202
+ f"{content}",
203
+ )
204
+ else:
205
+ table.add_row("", " ", f"{content}")
206
+
207
+ hunk_lines = []
208
+
209
+ for line in lines:
210
+ if line.startswith("diff --git") or line.startswith("index"):
211
+ # 跳过 diff 头部
212
+ continue
213
+ elif line.startswith("---"):
214
+ # 旧文件路径
215
+ old_path = line[4:].strip()
216
+ if not file_path and old_path != "/dev/null":
217
+ file_path = old_path
218
+ elif line.startswith("+++"):
219
+ # 新文件路径
220
+ new_path = line[4:].strip()
221
+ if new_path != "/dev/null":
222
+ file_path = new_path
223
+ elif line.startswith("@@"):
224
+ # Hunk 头部 - 刷新上一个 hunk
225
+ if in_hunk:
226
+ flush_hunk_context()
227
+
228
+ in_hunk = True
229
+ # 解析行号信息
230
+ parts = line.split("@@")
231
+ if len(parts) >= 2:
232
+ hunk_info = parts[1].strip()
233
+ if hunk_info:
234
+ # 解析格式: -old_start,old_count +new_start,new_count
235
+ old_part = ""
236
+ new_part = ""
237
+ for token in hunk_info.split():
238
+ if token.startswith("-"):
239
+ old_part = token[1:].split(",")[0]
240
+ elif token.startswith("+"):
241
+ new_part = token[1:].split(",")[0]
242
+
243
+ if old_part:
244
+ try:
245
+ old_line_num = int(old_part)
246
+ except ValueError:
247
+ pass
248
+ if new_part:
249
+ try:
250
+ new_line_num = int(new_part)
251
+ except ValueError:
252
+ pass
253
+
254
+ # 显示 hunk 头部
255
+ hunk_text = Text(f"[bright_cyan]{line}[/bright_cyan]", style="cyan")
256
+ if show_line_numbers:
257
+ table.add_row("", "", "", hunk_text)
258
+ else:
259
+ table.add_row("", "", hunk_text)
260
+ elif in_hunk:
261
+ if line.startswith("-"):
262
+ # 删除的行
263
+ content = line[1:] if len(line) > 1 else ""
264
+ hunk_lines.append(("-", old_line_num, None, content))
265
+ old_line_num += 1
266
+ elif line.startswith("+"):
267
+ # 新增的行
268
+ content = line[1:] if len(line) > 1 else ""
269
+ hunk_lines.append(("+", None, new_line_num, content))
270
+ new_line_num += 1
271
+ elif line.startswith(" "):
272
+ # 未更改的行(上下文)
273
+ content = line[1:] if len(line) > 1 else ""
274
+ hunk_lines.append((" ", old_line_num, new_line_num, content))
275
+ old_line_num += 1
276
+ new_line_num += 1
277
+ elif line.strip() == "\\":
278
+ # 文件末尾换行符差异
279
+ if show_line_numbers:
280
+ table.add_row(
281
+ "",
282
+ "",
283
+ "",
284
+ "[bright_yellow]\[/bright_yellow]",
285
+ )
286
+ else:
287
+ table.add_row(
288
+ "",
289
+ "",
290
+ "[bright_yellow]\[/bright_yellow]",
291
+ )
292
+
293
+ # 刷新最后一个 hunk
294
+ if in_hunk:
295
+ flush_hunk_context()
296
+
297
+ # 显示 diff 表格(包裹在 Panel 中)
298
+ if table.rows:
299
+ title = f"📝 {file_path}" if file_path else "Diff"
300
+ panel = Panel(table, title=title, border_style="cyan", padding=(0, 1))
301
+ self.console.print(panel)
302
+
303
+ def visualize_statistics(
304
+ self, file_path: str, additions: int, deletions: int, total_changes: int = 0
305
+ ) -> None:
306
+ """显示文件变更统计
307
+
308
+ 参数:
309
+ file_path: 文件路径
310
+ additions: 新增行数
311
+ deletions: 删除行数
312
+ total_changes: 总变更行数(如果为0则自动计算)
313
+ """
314
+ if total_changes == 0:
315
+ total_changes = additions + deletions
316
+
317
+ # 创建统计文本
318
+ stats_text = Text()
319
+ stats_text.append(f"📊 {file_path}\n", style="bold cyan")
320
+ stats_text.append(" ", style="bright_white")
321
+ stats_text.append("➕ 新增: ", style="green")
322
+ stats_text.append(f"{additions} 行", style="bold green")
323
+ stats_text.append(" | ", style="bright_white")
324
+ stats_text.append("➖ 删除: ", style="red")
325
+ stats_text.append(f"{deletions} 行", style="bold red")
326
+ if total_changes > 0:
327
+ stats_text.append(" | ", style="bright_white")
328
+ stats_text.append("📈 总计: ", style="cyan")
329
+ stats_text.append(f"{total_changes} 行", style="bold cyan")
330
+
331
+ panel = Panel(stats_text, border_style="cyan", padding=(1, 2))
332
+ self.console.print(panel)
333
+
334
+ def visualize_syntax_highlighted(
335
+ self, diff_text: str, file_path: str = "", theme: str = "monokai"
336
+ ) -> None:
337
+ """使用语法高亮显示 diff(保持原有风格但改进)
338
+
339
+ 参数:
340
+ diff_text: git diff 输出的文本
341
+ file_path: 文件路径
342
+ theme: 语法高亮主题
343
+ """
344
+ if not diff_text.strip():
345
+ return
346
+
347
+ # 使用 Rich 的 diff 语法高亮
348
+ syntax = Syntax(
349
+ diff_text,
350
+ "diff",
351
+ theme=theme,
352
+ line_numbers=True,
353
+ word_wrap=True,
354
+ background_color="default",
355
+ )
356
+
357
+ if file_path:
358
+ panel = Panel(
359
+ syntax,
360
+ title=f"📝 {file_path}",
361
+ border_style="cyan",
362
+ padding=(0, 1),
363
+ )
364
+ self.console.print(panel)
365
+ else:
366
+ self.console.print(syntax)
367
+
368
+ def visualize_compact(
369
+ self,
370
+ diff_text: str,
371
+ file_path: str = "",
372
+ max_lines: int = 50,
373
+ ) -> None:
374
+ """紧凑型 diff 显示(适合快速预览)
375
+
376
+ 参数:
377
+ diff_text: git diff 输出的文本
378
+ file_path: 文件路径
379
+ max_lines: 最大显示行数
380
+ """
381
+ if not diff_text.strip():
382
+ return
383
+
384
+ lines = diff_text.split("\n")
385
+ display_lines = lines[:max_lines]
386
+
387
+ # 统计信息
388
+ additions = sum(
389
+ 1
390
+ for line in display_lines
391
+ if line.startswith("+") and not line.startswith("+++")
392
+ )
393
+ deletions = sum(
394
+ 1
395
+ for line in display_lines
396
+ if line.startswith("-") and not line.startswith("---")
397
+ )
398
+
399
+ # 显示 diff(使用语法高亮,包裹在 Panel 中)
400
+ if len(lines) > max_lines:
401
+ remaining = len(lines) - max_lines
402
+ display_text = "\n".join(display_lines)
403
+ display_text += f"\n... ({remaining} 行已省略)"
404
+ else:
405
+ display_text = "\n".join(display_lines)
406
+
407
+ syntax = Syntax(
408
+ display_text,
409
+ "diff",
410
+ theme="monokai",
411
+ line_numbers=False,
412
+ word_wrap=True,
413
+ )
414
+
415
+ # 构建标题(包含统计信息)
416
+ title = f"📝 {file_path}" if file_path else "Diff"
417
+ if additions > 0 or deletions > 0:
418
+ title += f" [green]+{additions}[/green] / [red]-{deletions}[/red]"
419
+
420
+ panel = Panel(syntax, title=title, border_style="cyan", padding=(0, 1))
421
+ self.console.print(panel)
422
+
423
+ def visualize_side_by_side_summary(
424
+ self,
425
+ old_lines: List[str],
426
+ new_lines: List[str],
427
+ file_path: str = "",
428
+ context_lines: int = 3,
429
+ old_line_map: Optional[List[int]] = None,
430
+ new_line_map: Optional[List[int]] = None,
431
+ ) -> None:
432
+ """并排显示摘要(仅显示变更部分,智能配对)
433
+
434
+ 参数:
435
+ old_lines: 旧文件行列表
436
+ new_lines: 新文件行列表
437
+ file_path: 文件路径
438
+ context_lines: 上下文行数
439
+ old_line_map: 旧文件行号映射(索引对应 old_lines 索引,值为文件中的绝对行号)
440
+ new_line_map: 新文件行号映射(索引对应 new_lines 索引,值为文件中的绝对行号)
441
+ """
442
+ # 使用 difflib.SequenceMatcher 进行更精确的匹配
443
+ matcher = difflib.SequenceMatcher(None, old_lines, new_lines)
444
+ opcodes = matcher.get_opcodes()
445
+
446
+ # 如果没有提供行号映射,使用索引+1作为行号(向后兼容)
447
+ if old_line_map is None:
448
+ old_line_map = [i + 1 for i in range(len(old_lines))]
449
+ if new_line_map is None:
450
+ new_line_map = [i + 1 for i in range(len(new_lines))]
451
+
452
+ # 获取语言类型用于语法高亮
453
+ language = self._get_language_by_extension(file_path)
454
+
455
+ # 创建并排表格 - 使用最大可用宽度
456
+ table = Table(
457
+ show_header=True,
458
+ header_style="bold magenta",
459
+ box=None,
460
+ padding=(0, 0), # 移除内部padding让内容更紧凑
461
+ expand=True, # 让表格占满可用宽度
462
+ )
463
+ table.add_column("", style="bright_cyan", width=6, justify="right")
464
+ table.add_column(
465
+ "", style="bright_white", overflow="fold", ratio=10
466
+ ) # 增加ratio值获得更多空间
467
+ table.add_column("", style="bright_cyan", width=6, justify="right")
468
+ table.add_column(
469
+ "", style="bright_white", overflow="fold", ratio=10
470
+ ) # 增加ratio值获得更多空间
471
+
472
+ additions = 0
473
+ deletions = 0
474
+ has_changes = False
475
+
476
+ # 用于跟踪前一个变更块的结束位置
477
+ prev_change_end_old = 0
478
+ prev_change_end_new = 0
479
+
480
+ for idx, (tag, i1, i2, j1, j2) in enumerate(opcodes):
481
+ # 检测是否需要在变更块之间插入分隔符
482
+ if idx > 0 and tag in ("replace", "delete", "insert"):
483
+ # 使用实际行号检查连续性
484
+ prev_old_line = (
485
+ old_line_map[prev_change_end_old - 1]
486
+ if prev_change_end_old > 0
487
+ and prev_change_end_old - 1 < len(old_line_map)
488
+ else None
489
+ )
490
+ curr_old_line = old_line_map[i1] if i1 < len(old_line_map) else None
491
+ prev_new_line = (
492
+ new_line_map[prev_change_end_new - 1]
493
+ if prev_change_end_new > 0
494
+ and prev_change_end_new - 1 < len(new_line_map)
495
+ else None
496
+ )
497
+ curr_new_line = new_line_map[j1] if j1 < len(new_line_map) else None
498
+
499
+ # 只有当所有行号都有效且都不连续时才添加分割线
500
+ should_add_separator = False
501
+ if (
502
+ prev_old_line is not None
503
+ and curr_old_line is not None
504
+ and prev_new_line is not None
505
+ and curr_new_line is not None
506
+ ):
507
+ # 检查实际行号是否不连续(两个方向都不连续)
508
+ old_not_continuous = curr_old_line != prev_old_line + 1
509
+ new_not_continuous = curr_new_line != prev_new_line + 1
510
+
511
+ # 只有当两个方向都不连续时才添加分割线
512
+ should_add_separator = old_not_continuous and new_not_continuous
513
+
514
+ if should_add_separator:
515
+ # 动态计算分隔符宽度(基于终端宽度,限制在合理范围)
516
+ terminal_width = self.console.width or 120
517
+ # 表格有两列内容区域(每列约占总宽度的45%,减去行号列)
518
+ separator_width = max(20, min(50, int(terminal_width * 0.4)))
519
+ separator_text = "─" * separator_width
520
+ separator = Text(separator_text, style="bright_black dim")
521
+ table.add_row(
522
+ "",
523
+ separator,
524
+ "",
525
+ separator,
526
+ )
527
+
528
+ # 只有在处理变更块时才更新结束位置
529
+ if tag in ("replace", "delete", "insert"):
530
+ prev_change_end_old = i2
531
+ prev_change_end_new = j2
532
+
533
+ if tag == "equal":
534
+ # 显示未更改的行(灰色/dim样式),但只显示上下文行数
535
+ equal_chunk = old_lines[i1:i2]
536
+ equal_len = len(equal_chunk)
537
+
538
+ # 只显示最后 context_lines 行作为上下文
539
+ if equal_len > context_lines * 2:
540
+ # 如果 equal 块太长,只显示开头和结尾的上下文
541
+ if idx > 0: # 不是第一个块
542
+ # 显示开头的 context_lines 行
543
+ for k in range(min(context_lines, equal_len)):
544
+ old_line_num = (
545
+ old_line_map[i1 + k]
546
+ if i1 + k < len(old_line_map)
547
+ else i1 + k + 1
548
+ )
549
+ new_line_num = (
550
+ new_line_map[j1 + k]
551
+ if j1 + k < len(new_line_map)
552
+ else j1 + k + 1
553
+ )
554
+ old_syntax = Syntax(
555
+ equal_chunk[k],
556
+ language,
557
+ theme="monokai",
558
+ background_color="default",
559
+ )
560
+ new_syntax = Syntax(
561
+ equal_chunk[k],
562
+ language,
563
+ theme="monokai",
564
+ background_color="default",
565
+ )
566
+ table.add_row(
567
+ f"[bright_cyan]{old_line_num}[/bright_cyan]",
568
+ old_syntax,
569
+ f"[bright_cyan]{new_line_num}[/bright_cyan]",
570
+ new_syntax,
571
+ )
572
+ # 如果有省略,显示省略标记
573
+ if equal_len > context_lines * 2:
574
+ table.add_row(
575
+ "",
576
+ "[bright_yellow]... ({0} lines omitted) ...[/bright_yellow]".format(
577
+ equal_len - context_lines * 2
578
+ ),
579
+ "",
580
+ "",
581
+ )
582
+ # 显示结尾的 context_lines 行
583
+ for k in range(max(0, equal_len - context_lines), equal_len):
584
+ old_line_num = (
585
+ old_line_map[i1 + k]
586
+ if i1 + k < len(old_line_map)
587
+ else i1 + k + 1
588
+ )
589
+ new_line_num = (
590
+ new_line_map[j1 + k]
591
+ if j1 + k < len(new_line_map)
592
+ else j1 + k + 1
593
+ )
594
+ old_syntax = Syntax(
595
+ equal_chunk[k],
596
+ language,
597
+ theme="monokai",
598
+ background_color="default",
599
+ )
600
+ new_syntax = Syntax(
601
+ equal_chunk[k],
602
+ language,
603
+ theme="monokai",
604
+ background_color="default",
605
+ )
606
+ table.add_row(
607
+ f"[bright_cyan]{old_line_num}[/bright_cyan]",
608
+ old_syntax,
609
+ f"[bright_cyan]{new_line_num}[/bright_cyan]",
610
+ new_syntax,
611
+ )
612
+ else:
613
+ # 第一个块,只显示结尾的上下文
614
+ start_idx = max(0, equal_len - context_lines)
615
+ for k in range(start_idx, equal_len):
616
+ old_line_num = (
617
+ old_line_map[i1 + k]
618
+ if i1 + k < len(old_line_map)
619
+ else i1 + k + 1
620
+ )
621
+ new_line_num = (
622
+ new_line_map[j1 + k]
623
+ if j1 + k < len(new_line_map)
624
+ else j1 + k + 1
625
+ )
626
+ old_syntax = Syntax(
627
+ equal_chunk[k],
628
+ language,
629
+ theme="monokai",
630
+ background_color="default",
631
+ )
632
+ new_syntax = Syntax(
633
+ equal_chunk[k],
634
+ language,
635
+ theme="monokai",
636
+ background_color="default",
637
+ )
638
+ table.add_row(
639
+ f"[bright_cyan]{old_line_num}[/bright_cyan]",
640
+ old_syntax,
641
+ f"[bright_cyan]{new_line_num}[/bright_cyan]",
642
+ new_syntax,
643
+ )
644
+ else:
645
+ # 如果 equal 块不长,显示所有行
646
+ for k, line in enumerate(equal_chunk):
647
+ old_line_num = (
648
+ old_line_map[i1 + k]
649
+ if i1 + k < len(old_line_map)
650
+ else i1 + k + 1
651
+ )
652
+ new_line_num = (
653
+ new_line_map[j1 + k]
654
+ if j1 + k < len(new_line_map)
655
+ else j1 + k + 1
656
+ )
657
+ old_syntax = Syntax(
658
+ line,
659
+ language,
660
+ theme="monokai",
661
+ background_color="default",
662
+ )
663
+ new_syntax = Syntax(
664
+ line,
665
+ language,
666
+ theme="monokai",
667
+ background_color="default",
668
+ )
669
+ table.add_row(
670
+ f"[bright_cyan]{old_line_num}[/bright_cyan]",
671
+ old_syntax,
672
+ f"[bright_cyan]{new_line_num}[/bright_cyan]",
673
+ new_syntax,
674
+ )
675
+ continue
676
+ elif tag == "replace":
677
+ # 替换:删除的行和新增的行配对显示
678
+ old_chunk = old_lines[i1:i2]
679
+ new_chunk = new_lines[j1:j2]
680
+ deletions += len(old_chunk)
681
+ additions += len(new_chunk)
682
+ has_changes = True
683
+
684
+ # 配对显示
685
+ max_len = max(len(old_chunk), len(new_chunk))
686
+ for k in range(max_len):
687
+ # 文件中的绝对行号
688
+ if k < len(old_chunk):
689
+ old_line_num_actual: Union[int, str] = (
690
+ old_line_map[i1 + k]
691
+ if i1 + k < len(old_line_map)
692
+ else i1 + k + 1
693
+ )
694
+ old_replace_syntax: Union[Syntax, str] = Syntax(
695
+ old_chunk[k],
696
+ language,
697
+ theme="monokai",
698
+ background_color="#5c0000",
699
+ )
700
+ else:
701
+ old_line_num_actual = ""
702
+ old_replace_syntax = ""
703
+
704
+ if k < len(new_chunk):
705
+ new_line_num_actual: Union[int, str] = (
706
+ new_line_map[j1 + k]
707
+ if j1 + k < len(new_line_map)
708
+ else j1 + k + 1
709
+ )
710
+ new_replace_syntax: Union[Syntax, str] = Syntax(
711
+ new_chunk[k],
712
+ language,
713
+ theme="monokai",
714
+ background_color="#004d00",
715
+ )
716
+ else:
717
+ new_line_num_actual = ""
718
+ new_replace_syntax = ""
719
+
720
+ table.add_row(
721
+ str(old_line_num_actual),
722
+ old_replace_syntax,
723
+ str(new_line_num_actual),
724
+ new_replace_syntax,
725
+ )
726
+ elif tag == "delete":
727
+ # 仅删除
728
+ old_chunk = old_lines[i1:i2]
729
+ deletions += len(old_chunk)
730
+ has_changes = True
731
+ for k, line in enumerate(old_chunk):
732
+ # 文件中的绝对行号
733
+ old_line_num = (
734
+ old_line_map[i1 + k]
735
+ if i1 + k < len(old_line_map)
736
+ else i1 + k + 1
737
+ )
738
+ old_delete_syntax: Union[Syntax, str] = Syntax(
739
+ line, language, theme="monokai", background_color="#5c0000"
740
+ )
741
+ table.add_row(
742
+ str(old_line_num),
743
+ old_delete_syntax,
744
+ "",
745
+ "",
746
+ )
747
+ elif tag == "insert":
748
+ # 仅新增
749
+ new_chunk = new_lines[j1:j2]
750
+ additions += len(new_chunk)
751
+ has_changes = True
752
+ for k, line in enumerate(new_chunk):
753
+ # 文件中的绝对行号
754
+ new_line_num = (
755
+ new_line_map[j1 + k]
756
+ if j1 + k < len(new_line_map)
757
+ else j1 + k + 1
758
+ )
759
+ new_insert_syntax: Union[Syntax, str] = Syntax(
760
+ line, language, theme="monokai", background_color="#004d00"
761
+ )
762
+ table.add_row(
763
+ "",
764
+ "",
765
+ str(new_line_num),
766
+ new_insert_syntax,
767
+ )
768
+
769
+ # 如果没有变更,显示提示
770
+ if not has_changes:
771
+ self.console.print("[dim](无变更)[/dim]")
772
+ return
773
+
774
+ # 构建标题(包含统计信息)
775
+ title = f"📝 {file_path}" if file_path else "Side-by-Side Diff"
776
+ title += f" [bright_green]+{additions}[/bright_green] / [bright_red]-{deletions}[/bright_red]"
777
+
778
+ # 包裹在 Panel 中显示 - 最小化padding以最大化内容区域
779
+ panel = Panel(table, title=title, border_style="bright_cyan", padding=(0, 0))
780
+ self.console.print(panel)
781
+
782
+
783
+ def _split_diff_by_files(diff_text: str) -> List[tuple]:
784
+ """将包含多个文件的 diff 文本分割成单个文件的 diff
785
+
786
+ 参数:
787
+ diff_text: git diff 输出的文本(可能包含多个文件)
788
+
789
+ 返回:
790
+ List[tuple]: [(file_path, single_file_diff), ...] 列表
791
+ """
792
+ files = []
793
+ lines = diff_text.splitlines()
794
+ current_file_path = ""
795
+ current_file_lines: list = []
796
+
797
+ i = 0
798
+ while i < len(lines):
799
+ line = lines[i]
800
+
801
+ if line.startswith("diff --git"):
802
+ # 如果已经有文件在处理,先保存它
803
+ if current_file_lines:
804
+ files.append((current_file_path, "\n".join(current_file_lines)))
805
+
806
+ # 开始新文件
807
+ current_file_lines = [line]
808
+ # 尝试从 diff --git 行提取文件路径
809
+ # 格式: diff --git a/path b/path
810
+ parts = line.split()
811
+ if len(parts) >= 4:
812
+ # 取 b/path 部分,去掉 b/ 前缀
813
+ path_part = parts[3]
814
+ if path_part.startswith("b/"):
815
+ current_file_path = path_part[2:]
816
+ else:
817
+ current_file_path = path_part
818
+ else:
819
+ current_file_path = ""
820
+ elif line.startswith("---") or line.startswith("+++"):
821
+ # 更新文件路径(优先使用 +++ 行的路径)
822
+ current_file_lines.append(line)
823
+ if line.startswith("+++"):
824
+ path_part = line[4:].strip()
825
+ if path_part != "/dev/null":
826
+ # 去掉 a/ 或 b/ 前缀
827
+ if path_part.startswith("b/"):
828
+ current_file_path = path_part[2:]
829
+ elif path_part.startswith("a/"):
830
+ current_file_path = path_part[2:]
831
+ else:
832
+ current_file_path = path_part
833
+ else:
834
+ # 其他行添加到当前文件
835
+ current_file_lines.append(line)
836
+
837
+ i += 1
838
+
839
+ # 保存最后一个文件
840
+ if current_file_lines:
841
+ files.append((current_file_path, "\n".join(current_file_lines)))
842
+
843
+ # 如果没有找到任何文件分隔符,整个 diff 作为一个文件处理
844
+ if not files:
845
+ # 尝试从 --- 或 +++ 行提取文件路径
846
+ file_path = ""
847
+ for line in lines:
848
+ if line.startswith("+++"):
849
+ path_part = line[4:].strip()
850
+ if path_part != "/dev/null":
851
+ if path_part.startswith("b/"):
852
+ file_path = path_part[2:]
853
+ elif path_part.startswith("a/"):
854
+ file_path = path_part[2:]
855
+ else:
856
+ file_path = path_part
857
+ break
858
+
859
+ files.append((file_path, diff_text))
860
+
861
+ return files
862
+
863
+
864
+ def _parse_diff_to_lines(diff_text: str) -> tuple:
865
+ """从 git diff 文本中解析出旧文件和新文件的行列表(带行号信息)
866
+
867
+ 参数:
868
+ diff_text: git diff 输出的文本
869
+
870
+ 返回:
871
+ (old_lines, new_lines, old_line_map, new_line_map):
872
+ 旧文件行列表、新文件行列表、旧文件行号映射、新文件行号映射
873
+ 行号映射是一个列表,索引对应行列表的索引,值是该行在文件中的绝对行号
874
+ """
875
+ old_lines = []
876
+ new_lines = []
877
+ old_line_map = [] # 映射 old_lines 索引到文件中的绝对行号
878
+ new_line_map = [] # 映射 new_lines 索引到文件中的绝对行号
879
+
880
+ old_line_num = 0
881
+ new_line_num = 0
882
+
883
+ for line in diff_text.splitlines():
884
+ if line.startswith("@@"):
885
+ # 解析 hunk 头,获取起始行号
886
+ parts = line.split("@@")
887
+ if len(parts) >= 2:
888
+ hunk_info = parts[1].strip()
889
+ if hunk_info:
890
+ for token in hunk_info.split():
891
+ if token.startswith("-"):
892
+ try:
893
+ old_line_num = int(token[1:].split(",")[0])
894
+ except ValueError:
895
+ pass
896
+ elif token.startswith("+"):
897
+ try:
898
+ new_line_num = int(token[1:].split(",")[0])
899
+ except ValueError:
900
+ pass
901
+ continue
902
+ elif line.startswith("---") or line.startswith("+++"):
903
+ # 跳过文件头
904
+ continue
905
+ elif line.startswith("diff ") or line.startswith("index "):
906
+ # 跳过 diff 元信息
907
+ continue
908
+ elif line.startswith("-"):
909
+ # 删除的行
910
+ old_lines.append(line[1:])
911
+ old_line_map.append(old_line_num)
912
+ old_line_num += 1
913
+ elif line.startswith("+"):
914
+ # 新增的行
915
+ new_lines.append(line[1:])
916
+ new_line_map.append(new_line_num)
917
+ new_line_num += 1
918
+ elif line.startswith(" "):
919
+ # 未更改的行
920
+ old_lines.append(line[1:])
921
+ new_lines.append(line[1:])
922
+ old_line_map.append(old_line_num)
923
+ new_line_map.append(new_line_num)
924
+ old_line_num += 1
925
+ new_line_num += 1
926
+ else:
927
+ # 其他行(如空行)
928
+ old_lines.append(line)
929
+ new_lines.append(line)
930
+ old_line_map.append(old_line_num if old_line_num > 0 else 0)
931
+ new_line_map.append(new_line_num if new_line_num > 0 else 0)
932
+
933
+ return old_lines, new_lines, old_line_map, new_line_map
934
+
935
+
936
+ def visualize_diff_enhanced(
937
+ diff_text: str,
938
+ file_path: str = "",
939
+ mode: str = "unified",
940
+ show_line_numbers: bool = True,
941
+ context_lines: int = 3,
942
+ ) -> None:
943
+ """增强的 diff 可视化函数(便捷接口)
944
+
945
+ 参数:
946
+ diff_text: git diff 输出的文本
947
+ file_path: 文件路径
948
+ mode: 可视化模式 ("unified" | "syntax" | "compact" | "side_by_side" | "statistics")
949
+ show_line_numbers: 是否显示行号
950
+ context_lines: 上下文行数
951
+ """
952
+ visualizer = DiffVisualizer()
953
+
954
+ if mode == "unified":
955
+ visualizer.visualize_unified_diff(
956
+ diff_text,
957
+ file_path,
958
+ show_line_numbers=show_line_numbers,
959
+ context_lines=context_lines,
960
+ )
961
+ elif mode == "syntax":
962
+ visualizer.visualize_syntax_highlighted(diff_text, file_path)
963
+ elif mode == "compact":
964
+ visualizer.visualize_compact(diff_text, file_path)
965
+ elif mode == "side_by_side":
966
+ # 检查是否有多个文件
967
+ file_diffs = _split_diff_by_files(diff_text)
968
+
969
+ if len(file_diffs) > 1:
970
+ # 多个文件,为每个文件显示独立的 table
971
+ for single_file_path, single_file_diff in file_diffs:
972
+ old_lines, new_lines, old_line_map, new_line_map = _parse_diff_to_lines(
973
+ single_file_diff
974
+ )
975
+ visualizer.visualize_side_by_side_summary(
976
+ old_lines,
977
+ new_lines,
978
+ single_file_path if single_file_path else file_path,
979
+ context_lines=context_lines,
980
+ old_line_map=old_line_map,
981
+ new_line_map=new_line_map,
982
+ )
983
+ else:
984
+ # 单个文件,使用原有逻辑
985
+ old_lines, new_lines, old_line_map, new_line_map = _parse_diff_to_lines(
986
+ diff_text
987
+ )
988
+ visualizer.visualize_side_by_side_summary(
989
+ old_lines,
990
+ new_lines,
991
+ file_path,
992
+ context_lines=context_lines,
993
+ old_line_map=old_line_map,
994
+ new_line_map=new_line_map,
995
+ )
996
+ else:
997
+ # 默认使用语法高亮
998
+ visualizer.visualize_syntax_highlighted(diff_text, file_path)