T19 Git: diffs unified and split with word marks and folds, three-way merge and conflict resolver, changes list and commit input, commit graph and list, branch selector, branch, tag and stash lists, blame with annotation, file history, pull request card, review thread, diff stat and status badge

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-09-26 01:59:27 -04:00
1 parent de1c03746e
commit d083e87493
31 files changed
+4166 -21

No files matched your search

+217
View File
@@ -0,0 +1,217 @@
use std::ops::Range;
use gpui::SharedString;
use similar::{ChangeTag, TextDiff};
/// Whether a diff line stayed, came in, or went.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LineKind {
Same,
Added,
Removed,
}
/// A line of a diff: its kind, its numbers in the old and new text from one, its text, and the words that changed.
#[derive(Clone, Debug, PartialEq)]
pub struct DiffLine {
pub kind: LineKind,
pub old: Option<usize>,
pub new: Option<usize>,
pub text: SharedString,
pub words: Vec<Range<usize>>,
}
/// A stretch of a diff: changed lines in their context, or unchanged lines folded away between.
#[derive(Clone, Debug, PartialEq)]
pub enum Stretch {
Hunk {
/// `@@ -old +new @@`, from one.
header: SharedString,
lines: Vec<DiffLine>,
},
Folded(Vec<DiffLine>),
}
fn same(text: &str, old: usize, new: usize) -> DiffLine {
DiffLine {
kind: LineKind::Same,
old: Some(old + 1),
new: Some(new + 1),
text: text.trim_end_matches(['\n', '\r']).to_string().into(),
words: Vec::new(),
}
}
/// `old` against `new` line by line: hunks with `context` lines around each change, the rest folded.
pub fn diff(old: &str, new: &str, context: usize) -> Vec<Stretch> {
let diff = TextDiff::from_lines(old, new);
let (olds, news): (Vec<&str>, Vec<&str>) = (
diff.iter_old_slices().collect(),
diff.iter_new_slices().collect(),
);
let mut out = Vec::new();
let mut at = (0, 0);
for group in diff.grouped_ops(context) {
let (first, last) = (
group.first().expect("a group holds ops"),
group.last().expect("a group holds ops"),
);
let (old_start, new_start) = (first.old_range().start, first.new_range().start);
if old_start > at.0 {
out.push(Stretch::Folded(
(at.0..old_start)
.zip(at.1..new_start)
.map(|(old, new)| same(olds[old], old, new))
.collect(),
));
}
let (old_end, new_end) = (last.old_range().end, last.new_range().end);
let mut lines = Vec::new();
for op in &group {
for change in diff.iter_inline_changes(op) {
let (mut text, mut words) = (String::new(), Vec::new());
for (emphasized, part) in change.iter_strings_lossy() {
let part = part.trim_end_matches(['\n', '\r']);
if emphasized && !part.is_empty() {
words.push(text.len()..text.len() + part.len());
}
text.push_str(part);
}
let kind = match change.tag() {
ChangeTag::Equal => LineKind::Same,
ChangeTag::Insert => LineKind::Added,
ChangeTag::Delete => LineKind::Removed,
};
lines.push(DiffLine {
kind,
old: change.old_index().map(|ix| ix + 1),
new: change.new_index().map(|ix| ix + 1),
text: text.into(),
words: if kind == LineKind::Same {
Vec::new()
} else {
words
},
});
}
}
let header = format!(
"@@ -{},{} +{},{} @@",
old_start + 1,
old_end - old_start,
new_start + 1,
new_end - new_start
);
out.push(Stretch::Hunk {
header: header.into(),
lines,
});
at = (old_end, new_end);
}
if at.0 < olds.len() {
out.push(Stretch::Folded(
(at.0..olds.len())
.zip(at.1..news.len())
.map(|(old, new)| same(olds[old], old, new))
.collect(),
));
}
out
}
/// Lines added and removed from `old` to `new`.
pub fn stat(old: &str, new: &str) -> (usize, usize) {
let diff = TextDiff::from_lines(old, new);
diff.iter_all_changes()
.fold((0, 0), |(added, removed), change| match change.tag() {
ChangeTag::Insert => (added + 1, removed),
ChangeTag::Delete => (added, removed + 1),
ChangeTag::Equal => (added, removed),
})
}
/// A hunk's lines side by side: unchanged lines on both sides, each run of removals beside the additions after it.
pub fn pairs(lines: &[DiffLine]) -> Vec<(Option<&DiffLine>, Option<&DiffLine>)> {
let mut out = Vec::new();
let mut ix = 0;
while ix < lines.len() {
if lines[ix].kind == LineKind::Same {
out.push((Some(&lines[ix]), Some(&lines[ix])));
ix += 1;
continue;
}
let removed: Vec<&DiffLine> = lines[ix..]
.iter()
.take_while(|line| line.kind == LineKind::Removed)
.collect();
ix += removed.len();
let added: Vec<&DiffLine> = lines[ix..]
.iter()
.take_while(|line| line.kind == LineKind::Added)
.collect();
ix += added.len();
for row in 0..removed.len().max(added.len()) {
out.push((removed.get(row).copied(), added.get(row).copied()));
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_change_shows_its_words_and_numbers() {
let stretches = diff("a\nlet x = 1;\nc\n", "a\nlet x = 2;\nc\nd\n", 1);
let [Stretch::Hunk { header, lines }] = stretches.as_slice() else {
panic!("one hunk, not {stretches:?}");
};
assert_eq!(header.as_ref(), "@@ -1,3 +1,4 @@");
let kinds: Vec<LineKind> = lines.iter().map(|line| line.kind).collect();
use LineKind::*;
assert_eq!(kinds, [Same, Removed, Added, Same, Added]);
assert_eq!(&lines[1].text[lines[1].words[0].clone()], "1");
assert_eq!((lines[2].old, lines[2].new), (None, Some(2)));
assert_eq!((lines[4].old, lines[4].new), (None, Some(4)));
}
#[test]
fn unchanged_lines_between_hunks_fold() {
let old: String = (1..=20).map(|n| format!("{n}\n")).collect();
let new = old
.replacen("2\n", "two\n", 1)
.replace("19\n", "nineteen\n");
let stretches = diff(&old, &new, 1);
let shape: Vec<&str> = stretches
.iter()
.map(|stretch| match stretch {
Stretch::Hunk { .. } => "hunk",
Stretch::Folded(_) => "folded",
})
.collect();
assert_eq!(shape, ["hunk", "folded", "hunk"]);
let Stretch::Folded(hidden) = &stretches[1] else {
unreachable!()
};
assert_eq!((hidden.len(), hidden[0].old), (14, Some(4)));
assert_eq!(stat(&old, &new), (2, 2));
}
#[test]
fn removals_sit_beside_the_additions_after_them() {
let stretches = diff("a\nb\nc\n", "a\nB\nC\nD\n", 0);
let lines = stretches
.iter()
.find_map(|stretch| match stretch {
Stretch::Hunk { lines, .. } => Some(lines),
Stretch::Folded(_) => None,
})
.expect("a hunk");
let sides: Vec<(bool, bool)> = pairs(lines)
.iter()
.map(|(left, right)| (left.is_some(), right.is_some()))
.collect();
assert_eq!(sides, [(true, true), (true, true), (false, true)]);
}
}