Forge
csharp3407750f
1using System.Text.RegularExpressions;
2using AgentForge.Contracts;
3using AgentForge.Plugin.Sdk;
4
5namespace AgentForge.Plugin.View.Components.Diff;
6
7public static partial class ForgeViewDiffPanel
8{
9 public static string Render(MergeRequestDiffResponse diff, ForgeDiffRenderContext? lens = null)
10 {
11 var parts = new List<string>
12 {
13 ForgeHtml.P("meta diff-summary", ForgeHtml.Text(ForgePluginDiffFormatting.FormatSummary(diff.Stats))),
14 };
15
16 if (diff.Files.Count == 0)
17 {
18 parts.Add(ForgeHtml.P("meta", ForgeHtml.Text("(empty)")));
19 return ForgeHtml.Fragment([.. parts]);
20 }
21
22 foreach (var file in diff.Files)
23 parts.Add(RenderFileDiff(file, lens));
24
25 return ForgeHtml.Fragment([.. parts]);
26 }
27
28 private static string RenderFileDiff(DiffFileResponse file, ForgeDiffRenderContext? lens)
29 {
30 var headerParts = new List<string>();
31
32 if (lens is not null && !string.IsNullOrWhiteSpace(file.Path))
33 {
34 headerParts.Add(ForgeHtml.ARaw(
35 ForgeViewLinks.BlobHref(lens.RepoName, lens.SourceBranch, file.Path),
36 "diff-file-link",
37 null,
38 null,
39 ForgeHtml.Code(file.Path)));
40 }
41 else
42 {
43 headerParts.Add(ForgeHtml.Code(file.Path));
44 }
45
46 if (!string.IsNullOrWhiteSpace(file.OldPath)
47 && !string.Equals(file.OldPath, file.Path, StringComparison.Ordinal))
48 {
49 headerParts.Add(ForgeHtml.Text(" "));
50 headerParts.Add(ForgeHtml.Span("meta", ForgeHtml.Text($"← {file.OldPath}")));
51 }
52
53 headerParts.Add(ForgeHtml.Text(" "));
54 headerParts.Add(ForgeHtml.Badge(file.Status));
55
56 if (file.Additions > 0 || file.Deletions > 0)
57 {
58 var stats = new List<string>();
59 if (file.Additions > 0)
60 stats.Add(ForgeHtml.Span("diff-stat-add", ForgeHtml.Text($"+{file.Additions}")));
61 if (file.Deletions > 0)
62 stats.Add(ForgeHtml.Span("diff-stat-del", ForgeHtml.Text($"-{file.Deletions}")));
63 headerParts.Add(ForgeHtml.Text(" "));
64 headerParts.Add(ForgeHtml.Span("diff-stats", [.. stats]));
65 }
66
67 var body = string.IsNullOrWhiteSpace(file.Patch)
68 ? ForgeHtml.P("meta diff-empty", ForgeHtml.Text("(no textual patch)"))
69 : ForgeHtml.Table("diff-table", ForgeHtml.Tbody(RenderPatchRows(file.Patch, lens, file.Path)));
70
71 return ForgeHtml.Div("diff-file",
72 ForgeHtml.Div("diff-file-header", [.. headerParts]),
73 body);
74 }
75
76 private static string[] RenderPatchRows(string patch, ForgeDiffRenderContext? lens, string filePath)
77 {
78 var rows = new List<string>();
79 int? oldLine = null;
80 int? newLine = null;
81
82 foreach (var rawLine in patch.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n'))
83 {
84 if (rawLine.Length == 0)
85 continue;
86
87 if (rawLine.StartsWith("@@ ", StringComparison.Ordinal))
88 {
89 var hunk = HunkHeaderRegex().Match(rawLine);
90 if (hunk.Success)
91 {
92 var oldStart = int.Parse(hunk.Groups["oldStart"].Value);
93 var newStart = int.Parse(hunk.Groups["newStart"].Value);
94 oldLine = oldStart == 0 ? null : oldStart;
95 newLine = newStart == 0 ? null : newStart;
96 }
97
98 rows.Add(ForgeHtml.Tr("diff-hunk", null,
99 ForgeHtml.TdColspan(4, "diff-hunk-header", ForgeHtml.Text(rawLine))));
100 continue;
101 }
102
103 if (rawLine.StartsWith("\\ No newline", StringComparison.Ordinal))
104 {
105 rows.Add(ForgeHtml.Tr("diff-meta", null,
106 ForgeHtml.TdColspan(4, "diff-meta-text", ForgeHtml.Text(rawLine))));
107 continue;
108 }
109
110 var kind = rawLine[0];
111 var text = rawLine.Length > 1 ? rawLine[1..] : string.Empty;
112 switch (kind)
113 {
114 case '+':
115 rows.Add(RenderLineRow(null, newLine, '+', text, "diff-add", lens, filePath));
116 newLine = (newLine ?? 0) + 1;
117 break;
118 case '-':
119 rows.Add(RenderLineRow(oldLine, null, '-', text, "diff-del", lens, filePath));
120 oldLine = (oldLine ?? 0) + 1;
121 break;
122 case ' ':
123 rows.Add(RenderLineRow(oldLine, newLine, ' ', text, "diff-context", lens, filePath));
124 oldLine = (oldLine ?? 0) + 1;
125 newLine = (newLine ?? 0) + 1;
126 break;
127 default:
128 rows.Add(ForgeHtml.Tr("diff-meta", null,
129 ForgeHtml.TdColspan(4, "diff-meta-text", ForgeHtml.Text(rawLine))));
130 break;
131 }
132 }
133
134 return [.. rows];
135 }
136
137 private static string RenderLineRow(
138 int? oldNum,
139 int? newNum,
140 char marker,
141 string text,
142 string rowClass,
143 ForgeDiffRenderContext? lens,
144 string filePath)
145 {
146 var oldCell = oldNum is > 0 ? ForgeHtml.Text(oldNum.Value.ToString()) : "";
147 string newCell;
148 if (newNum is > 0)
149 {
150 newCell = lens is not null
151 ? ForgeHtml.A(
152 ForgeViewLinks.BlobHref(lens.RepoName, lens.SourceBranch, filePath, newNum.Value),
153 newNum.Value.ToString(),
154 "diff-num-link")
155 : ForgeHtml.Text(newNum.Value.ToString());
156 }
157 else
158 {
159 newCell = "";
160 }
161
162 return ForgeHtml.Tr($"diff-line {rowClass}", null,
163 ForgeHtml.Td("diff-num diff-num-old", oldCell),
164 ForgeHtml.Td("diff-num diff-num-new", newCell),
165 ForgeHtml.Td("diff-marker", ForgeHtml.Text(marker.ToString())),
166 ForgeHtml.Td("diff-code", ForgeHtml.Text(text)));
167 }
168
169 [GeneratedRegex(@"^@@ -(?<oldStart>\d+)(?:,\d+)? \+(?<newStart>\d+)(?:,\d+)? @@")]
170 private static partial Regex HunkHeaderRegex();
171}
172
View only · write via MCP/CIDE