use super::*;
#[derive(Clone, Copy)]
pub struct FileAt<'a> {
pub repo: &'a RepoId,
pub rev: &'a str,
pub path: &'a str,
pub lines: Option<(u32, u32)>,
}
pub fn file(page: &mut Page, at: FileAt<'_>, blob: &Blob, keys: Keys<'_>) {
let FileAt {
repo,
rev,
path,
lines: marked,
} = at;
code_toolbar(page, repo, rev, path, true, None, keys);
let size = size(blob.size);
let Some(text) = &blob.text else {
let title = meta(size);
empty_box(
page,
title,
"Binary file not shown here: open it on GitHub.",
);
return;
};
let lines = text.lines().count() as u64;
let mut info = format!("{} · {size}", plural(lines, "line"));
if blob.truncated {
info.push_str(" · only the beginning is shown");
}
let blame = url::blame(repo, rev, path);
let blame = link_seg(page, "Blame", blame, Role::Link);
let raw = link_seg(page, "Raw", url::raw(repo, rev, path), Role::Link);
let right = vec![blame, meta(" "), raw];
page.box_top(vec![meta(info)], right);
if is_markdown(path) {
let base = LinkBase::new(repo, rev, path);
markdown::render(page, text, Some(&base), Frame::Body);
page.box_bottom();
return;
}
let lines = markdown::highlighted_file(text, crate::diff::Language::from_path(path));
let digits = lines.len().to_string().len();
let is_marked = |n: usize| marked.is_some_and(|(a, b)| (a as usize..=b as usize).contains(&n));
let file_url = url::blob(repo, rev, path);
let line_url = |n: usize| format!("{file_url}#L{n}");
for (i, mut segs) in lines.into_iter().enumerate() {
let n = i + 1;
segs.insert(0, line_number(n, digits));
let start = code_line(page, segs, is_marked(n), 1);
let link = page.link(line_url(n));
page.item(start, link);
}
page.box_bottom();
}
pub fn blame(
page: &mut Page,
at: FileAt<'_>,
blob: Fetched<'_, Blob>,
blame: Fetched<'_, Blame>,
keys: Keys<'_>,
now: u64,
) {
let FileAt {
repo,
rev,
path,
lines: marked,
} = at;
code_toolbar(page, repo, rev, path, true, None, keys);
let Some(blob) = blob.show(page, "the file") else {
return;
};
let Some(blame) = blame.show(page, "the blame") else {
return;
};
let Some(text) = &blob.text else {
empty_box(page, meta("Blame"), "Binary file not shown.");
return;
};
let file = link_seg(page, "File", url::blob(repo, rev, path), Role::Link);
let commits = unique(blame.ranges.iter().map(|r| &r.commit.oid)).len() as u64;
let info = format!("Blame · {}", plural(commits, "commit"));
page.box_top(vec![meta(info)], vec![file]);
let lines = markdown::highlighted_file(text, crate::diff::Language::from_path(path));
let digits = lines.len().to_string().len();
let is_marked = |n: usize| marked.is_some_and(|(a, b)| (a as usize..=b as usize).contains(&n));
let author_w = (blame.ranges.iter())
.map(|r| width(r.commit.author.name()))
.max()
.unwrap_or(0)
.min(20);
let gutter_w = (usize::from(page.width) * 2 / 5).clamp(32, 90);
let fixed_w = 7 + 1 + author_w + 1 + 8 + 2;
let message_w = gutter_w.saturating_sub(fixed_w + 2);
let gutter_w = if message_w >= 8 { gutter_w } else { fixed_w };
let mut runs = vec![None; lines.len()];
for r in &blame.ranges {
let lines = (r.start as usize).saturating_sub(1)..r.end as usize;
for run in runs.iter_mut().take(lines.end).skip(lines.start) {
run.get_or_insert(r);
}
}
let mut run_start = 0;
for (i, mut segs) in lines.into_iter().enumerate() {
let n = i + 1;
let range = runs.get(i).copied().flatten();
if range.is_some_and(|r| r.start as usize == n && n > 1) {
page.box_rule();
}
let mut gutter = match range {
Some(r) if r.start as usize == n => {
run_start = page.lines.len();
let c = &r.commit;
let author = truncate(c.author.name(), author_w);
let mut gutter = vec![
link_seg(
page,
short_sha(&c.oid),
url::commit(repo, &c.oid),
Role::Code,
),
meta(format!(
" {author:<author_w$} {:>8} ",
time::ago_iso(&c.date, now)
)),
];
if message_w >= 8 {
let message = truncate(&c.headline, message_w);
gutter.push(Seg::new(format!("{message:<message_w$} "), Role::Body));
}
gutter
}
_ => vec![meta(" ".repeat(gutter_w))],
};
let fixed = u8::try_from(gutter.len() + 1).unwrap_or(u8::MAX);
gutter.push(line_number(n, digits));
gutter.append(&mut segs);
code_line(page, gutter, is_marked(n), fixed);
if let Some(r) = range.filter(|r| r.end as usize == n) {
let link = page.link(url::commit(repo, &r.commit.oid));
page.item(run_start, link);
}
}
page.box_bottom();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_blame_jumps_to_its_line() {
let range = |start, end, oid: &str| crate::api::browse::BlameRange {
start,
end,
age: 1,
commit: CommitInfo {
oid: oid.into(),
headline: String::new(),
author: Person::User(crate::api::browse::Login::unchecked("octocat")),
date: "2026-10-01T00:00:00Z".into(),
},
};
let blame = Blame {
ranges: vec![range(1, 2, "aaaaaaa"), range(3, 3, "bbbbbbb")],
};
let blob = crate::fixtures::file("a.txt", "one\ntwo\nthree\n");
let repo = RepoId::new("o", "r");
let at = FileAt {
repo: &repo,
rev: "main",
path: "a.txt",
lines: Some((3, 3)),
};
let mut page = Page::new(100);
let (blob, blame) = (Fetched::ready(&blob), Fetched::ready(&blame));
super::blame(&mut page, at, blob, blame, Keys::default(), 0);
let line = page
.jump
.and_then(|j| page.lines.get(j))
.map(PageLine::text);
assert!(
line.as_deref().is_some_and(|l| l.contains("three")),
"{line:?}"
);
}
}