| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 7 | |
| 8 | |
| 9 | |
| 10 | |
| 11 | |
| 12 | |
| 13 | use highlight::Line; |
| 14 | use maud::{Markup, html}; |
| 15 | |
| 16 | |
| 17 | pub struct RenderedFile { |
| 18 | |
| 19 | pub diff: git::FileDiff, |
| 20 | |
| 21 | pub old_lines: Vec<Line>, |
| 22 | |
| 23 | pub new_lines: Vec<Line>, |
| 24 | } |
| 25 | |
| 26 | impl RenderedFile { |
| 27 | |
| 28 | #[must_use] |
| 29 | pub fn path(&self) -> &str { |
| 30 | self.diff |
| 31 | .new_path |
| 32 | .as_deref() |
| 33 | .or(self.diff.old_path.as_deref()) |
| 34 | .unwrap_or("(unknown)") |
| 35 | } |
| 36 | |
| 37 | |
| 38 | #[must_use] |
| 39 | pub fn stats(&self) -> (usize, usize) { |
| 40 | let mut adds = 0; |
| 41 | let mut dels = 0; |
| 42 | for hunk in &self.diff.hunks { |
| 43 | for line in &hunk.lines { |
| 44 | match line.origin { |
| 45 | git::LineOrigin::Addition => adds += 1, |
| 46 | git::LineOrigin::Deletion => dels += 1, |
| 47 | git::LineOrigin::Context => {} |
| 48 | } |
| 49 | } |
| 50 | } |
| 51 | (adds, dels) |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | |
| 56 | |
| 57 | fn line_markup(lines: &[Line], n: u32, fallback: &str) -> Markup { |
| 58 | let idx = (n as usize).saturating_sub(1); |
| 59 | if let Some(line) = lines.get(idx) { |
| 60 | html! { |
| 61 | @for span in &line.spans { |
| 62 | @match span.class { |
| 63 | Some(class) => { span class=(class) { (span.text) } } |
| 64 | None => { (span.text) } |
| 65 | } |
| 66 | } |
| 67 | } |
| 68 | } else { |
| 69 | html! { (fallback.trim_end_matches(['\n', '\r'])) } |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | |
| 74 | |
| 75 | |
| 76 | |
| 77 | |
| 78 | #[must_use] |
| 79 | pub fn unified(rf: &RenderedFile, hx_base: Option<&str>) -> Markup { |
| 80 | html! { |
| 81 | table class="diff diff-unified" { |
| 82 | tbody { |
| 83 | @for (i, hunk) in rf.diff.hunks.iter().enumerate() { |
| 84 | @if let Some(base) = hx_base { |
| 85 | @if let Some((from, to)) = gap_before(rf, i) { |
| 86 | (expander(base, from, to)) |
| 87 | } |
| 88 | } |
| 89 | tr class="hunk-header" { |
| 90 | td colspan="3" class="lineno" {} |
| 91 | td class="code-line" { (hunk.header) } |
| 92 | } |
| 93 | @for line in &hunk.lines { |
| 94 | @let (cls, marker) = row_kind(line.origin); |
| 95 | tr class=(cls) { |
| 96 | td class="lineno" { (opt_num(line.old_lineno)) } |
| 97 | td class="lineno" { (opt_num(line.new_lineno)) } |
| 98 | td class="diff-marker" { (marker) } |
| 99 | td class="code-line" { (unified_line(rf, line)) } |
| 100 | } |
| 101 | } |
| 102 | } |
| 103 | } |
| 104 | } |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | |
| 109 | fn gap_before(rf: &RenderedFile, i: usize) -> Option<(u32, u32)> { |
| 110 | let first_new = rf |
| 111 | .diff |
| 112 | .hunks |
| 113 | .get(i)? |
| 114 | .lines |
| 115 | .iter() |
| 116 | .find_map(|l| l.new_lineno)?; |
| 117 | let prev_last = if i == 0 { |
| 118 | 0 |
| 119 | } else { |
| 120 | rf.diff.hunks[i - 1] |
| 121 | .lines |
| 122 | .iter() |
| 123 | .filter_map(|l| l.new_lineno) |
| 124 | .next_back() |
| 125 | .unwrap_or(0) |
| 126 | }; |
| 127 | if first_new > prev_last + 1 { |
| 128 | Some((prev_last + 1, first_new - 1)) |
| 129 | } else { |
| 130 | None |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | |
| 135 | fn expander(hx_base: &str, from: u32, to: u32) -> Markup { |
| 136 | let url = format!("{hx_base}&from={from}&to={to}"); |
| 137 | html! { |
| 138 | tr class="diff-expander" { |
| 139 | td colspan="4" { |
| 140 | button type="button" class="expand-btn" |
| 141 | hx-get=(url) hx-target="closest tr" hx-swap="outerHTML" { |
| 142 | "Expand " (to - from + 1) " hidden lines" |
| 143 | } |
| 144 | } |
| 145 | } |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | |
| 150 | fn unified_line(rf: &RenderedFile, line: &git::DiffLine) -> Markup { |
| 151 | match line.origin { |
| 152 | git::LineOrigin::Deletion => { |
| 153 | line_markup(&rf.old_lines, line.old_lineno.unwrap_or(0), &line.content) |
| 154 | } |
| 155 | |
| 156 | _ => line_markup(&rf.new_lines, line.new_lineno.unwrap_or(0), &line.content), |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | |
| 161 | #[must_use] |
| 162 | pub fn split(rf: &RenderedFile) -> Markup { |
| 163 | html! { |
| 164 | table class="diff diff-split" { |
| 165 | tbody { |
| 166 | @for hunk in &rf.diff.hunks { |
| 167 | tr class="hunk-header" { |
| 168 | td colspan="4" class="code-line" { (hunk.header) } |
| 169 | } |
| 170 | @for row in pair_hunk(hunk) { |
| 171 | tr { |
| 172 | @match &row.left { |
| 173 | Some(l) => { |
| 174 | td class="lineno" { (l.0) } |
| 175 | td class=(format!("code-line {}", side_class(l.1))) { |
| 176 | (line_markup(&rf.old_lines, l.0, &l.2)) |
| 177 | } |
| 178 | } |
| 179 | None => { td class="lineno" {} td class="code-line diff-empty" {} } |
| 180 | } |
| 181 | @match &row.right { |
| 182 | Some(r) => { |
| 183 | td class="lineno" { (r.0) } |
| 184 | td class=(format!("code-line {}", side_class(r.1))) { |
| 185 | (line_markup(&rf.new_lines, r.0, &r.2)) |
| 186 | } |
| 187 | } |
| 188 | None => { td class="lineno" {} td class="code-line diff-empty" {} } |
| 189 | } |
| 190 | } |
| 191 | } |
| 192 | } |
| 193 | } |
| 194 | } |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | |
| 199 | |
| 200 | struct SplitRow { |
| 201 | left: Option<(u32, git::LineOrigin, String)>, |
| 202 | right: Option<(u32, git::LineOrigin, String)>, |
| 203 | } |
| 204 | |
| 205 | |
| 206 | |
| 207 | fn pair_hunk(hunk: &git::Hunk) -> Vec<SplitRow> { |
| 208 | let mut rows = Vec::new(); |
| 209 | let mut dels: Vec<(u32, String)> = Vec::new(); |
| 210 | let mut adds: Vec<(u32, String)> = Vec::new(); |
| 211 | |
| 212 | let flush = |
| 213 | |rows: &mut Vec<SplitRow>, dels: &mut Vec<(u32, String)>, adds: &mut Vec<(u32, String)>| { |
| 214 | let n = dels.len().max(adds.len()); |
| 215 | for i in 0..n { |
| 216 | rows.push(SplitRow { |
| 217 | left: dels |
| 218 | .get(i) |
| 219 | .map(|(ln, c)| (*ln, git::LineOrigin::Deletion, c.clone())), |
| 220 | right: adds |
| 221 | .get(i) |
| 222 | .map(|(ln, c)| (*ln, git::LineOrigin::Addition, c.clone())), |
| 223 | }); |
| 224 | } |
| 225 | dels.clear(); |
| 226 | adds.clear(); |
| 227 | }; |
| 228 | |
| 229 | for line in &hunk.lines { |
| 230 | match line.origin { |
| 231 | git::LineOrigin::Deletion => { |
| 232 | dels.push((line.old_lineno.unwrap_or(0), line.content.clone())); |
| 233 | } |
| 234 | git::LineOrigin::Addition => { |
| 235 | adds.push((line.new_lineno.unwrap_or(0), line.content.clone())); |
| 236 | } |
| 237 | git::LineOrigin::Context => { |
| 238 | flush(&mut rows, &mut dels, &mut adds); |
| 239 | rows.push(SplitRow { |
| 240 | left: Some(( |
| 241 | line.old_lineno.unwrap_or(0), |
| 242 | git::LineOrigin::Context, |
| 243 | line.content.clone(), |
| 244 | )), |
| 245 | right: Some(( |
| 246 | line.new_lineno.unwrap_or(0), |
| 247 | git::LineOrigin::Context, |
| 248 | line.content.clone(), |
| 249 | )), |
| 250 | }); |
| 251 | } |
| 252 | } |
| 253 | } |
| 254 | flush(&mut rows, &mut dels, &mut adds); |
| 255 | rows |
| 256 | } |
| 257 | |
| 258 | |
| 259 | fn row_kind(origin: git::LineOrigin) -> (&'static str, &'static str) { |
| 260 | match origin { |
| 261 | git::LineOrigin::Addition => ("diff-add", "+"), |
| 262 | git::LineOrigin::Deletion => ("diff-del", "-"), |
| 263 | git::LineOrigin::Context => ("diff-ctx", " "), |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | |
| 268 | fn side_class(origin: git::LineOrigin) -> &'static str { |
| 269 | match origin { |
| 270 | git::LineOrigin::Addition => "diff-add", |
| 271 | git::LineOrigin::Deletion => "diff-del", |
| 272 | git::LineOrigin::Context => "diff-ctx", |
| 273 | } |
| 274 | } |
| 275 | |
| 276 | |
| 277 | fn opt_num(n: Option<u32>) -> String { |
| 278 | n.map_or_else(String::new, |n| n.to_string()) |
| 279 | } |
| 280 | |
| 281 | |
| 282 | #[must_use] |
| 283 | pub fn context_rows(lines: &[git::DiffLine], highlighted: &[Line]) -> Markup { |
| 284 | html! { |
| 285 | @for line in lines { |
| 286 | tr class="diff-ctx" { |
| 287 | td class="lineno" { (opt_num(line.old_lineno)) } |
| 288 | td class="lineno" { (opt_num(line.new_lineno)) } |
| 289 | td class="diff-marker" {} |
| 290 | td class="code-line" { |
| 291 | (line_markup(highlighted, line.new_lineno.unwrap_or(0), &line.content)) |
| 292 | } |
| 293 | } |
| 294 | } |
| 295 | } |
| 296 | } |