Git · DiffViewer: stretches made elsewhere, a hunk button, pickable changed lines, words marked on built lines; its words read i18n
This commit is contained in:
5 files changed
+357
-67
No files matched your search
+84
-14
@@ -124,33 +124,79 @@ pub fn stat(old: &str, new: &str) -> (usize, usize) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A hunk's lines side by side: unchanged lines on both sides, each run of removals beside the additions after it.
|
/// A hunk's rows side by side, as places in `lines`.
|
||||||
pub fn pairs(lines: &[DiffLine]) -> Vec<(Option<&DiffLine>, Option<&DiffLine>)> {
|
pub(crate) fn pair_places(lines: &[DiffLine]) -> Vec<(Option<usize>, Option<usize>)> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
let mut ix = 0;
|
let mut ix = 0;
|
||||||
while ix < lines.len() {
|
while ix < lines.len() {
|
||||||
if lines[ix].kind == LineKind::Same {
|
if lines[ix].kind == LineKind::Same {
|
||||||
out.push((Some(&lines[ix]), Some(&lines[ix])));
|
out.push((Some(ix), Some(ix)));
|
||||||
ix += 1;
|
ix += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let removed: Vec<&DiffLine> = lines[ix..]
|
let run = |from: usize, kind| {
|
||||||
.iter()
|
from..from
|
||||||
.take_while(|line| line.kind == LineKind::Removed)
|
+ lines[from..]
|
||||||
.collect();
|
.iter()
|
||||||
ix += removed.len();
|
.take_while(|line| line.kind == kind)
|
||||||
let added: Vec<&DiffLine> = lines[ix..]
|
.count()
|
||||||
.iter()
|
};
|
||||||
.take_while(|line| line.kind == LineKind::Added)
|
let removed = run(ix, LineKind::Removed);
|
||||||
.collect();
|
let added = run(removed.end, LineKind::Added);
|
||||||
ix += added.len();
|
ix = added.end;
|
||||||
for row in 0..removed.len().max(added.len()) {
|
for row in 0..removed.len().max(added.len()) {
|
||||||
out.push((removed.get(row).copied(), added.get(row).copied()));
|
let at = |range: &Range<usize>| (row < range.len()).then(|| range.start + row);
|
||||||
|
out.push((at(&removed), at(&added)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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>)> {
|
||||||
|
pair_places(lines)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(left, right)| (left.map(|ix| &lines[ix]), right.map(|ix| &lines[ix])))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks the words each removal and the addition beside it changed.
|
||||||
|
pub fn mark_words(lines: &mut [DiffLine]) {
|
||||||
|
for (left, right) in pair_places(lines) {
|
||||||
|
let (Some(left), Some(right)) = (left, right) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if left == right {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (old, new) = (lines[left].text.clone(), lines[right].text.clone());
|
||||||
|
let diff = TextDiff::from_unicode_words(old.as_ref(), new.as_ref());
|
||||||
|
let (mut at, mut words, mut kept) = ((0, 0), (Vec::new(), Vec::new()), false);
|
||||||
|
for change in diff.iter_all_changes() {
|
||||||
|
let (value, len) = (change.value(), change.value().len());
|
||||||
|
let real = !value.trim().is_empty();
|
||||||
|
match change.tag() {
|
||||||
|
ChangeTag::Equal => {
|
||||||
|
kept |= real;
|
||||||
|
at = (at.0 + len, at.1 + len);
|
||||||
|
}
|
||||||
|
ChangeTag::Delete => {
|
||||||
|
words.0.extend(real.then(|| at.0..at.0 + len));
|
||||||
|
at.0 += len;
|
||||||
|
}
|
||||||
|
ChangeTag::Insert => {
|
||||||
|
words.1.extend(real.then(|| at.1..at.1 + len));
|
||||||
|
at.1 += len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Lines with nothing in common mark no words.
|
||||||
|
if kept {
|
||||||
|
(lines[left].words, lines[right].words) = words;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The numbers a side-by-side row shows: the old file's on the left, the new file's on the right.
|
/// The numbers a side-by-side row shows: the old file's on the left, the new file's on the right.
|
||||||
pub(crate) fn side_numbers(
|
pub(crate) fn side_numbers(
|
||||||
left: Option<&DiffLine>,
|
left: Option<&DiffLine>,
|
||||||
@@ -220,6 +266,30 @@ mod tests {
|
|||||||
assert_eq!(sides, [(true, true), (true, true), (false, true)]);
|
assert_eq!(sides, [(true, true), (true, true), (false, true)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn words_are_marked_on_lines_built_elsewhere() {
|
||||||
|
let line = |kind, text: &str| DiffLine {
|
||||||
|
kind,
|
||||||
|
old: None,
|
||||||
|
new: None,
|
||||||
|
text: text.to_string().into(),
|
||||||
|
words: Vec::new(),
|
||||||
|
};
|
||||||
|
let mut lines = vec![
|
||||||
|
line(LineKind::Removed, "let x = 1;"),
|
||||||
|
line(LineKind::Removed, "abc"),
|
||||||
|
line(LineKind::Added, "let x = 2;"),
|
||||||
|
line(LineKind::Added, "xyz"),
|
||||||
|
];
|
||||||
|
mark_words(&mut lines);
|
||||||
|
assert_eq!(&lines[0].text[lines[0].words[0].clone()], "1");
|
||||||
|
assert_eq!(&lines[2].text[lines[2].words[0].clone()], "2");
|
||||||
|
assert!(
|
||||||
|
lines[1].words.is_empty() && lines[3].words.is_empty(),
|
||||||
|
"nothing shared"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn each_side_numbers_its_own_file() {
|
fn each_side_numbers_its_own_file() {
|
||||||
let stretches = diff("a\nb\n", "x\na\nb\n", 3);
|
let stretches = diff("a\nb\n", "x\na\nb\n", 3);
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ pub use badges::{DiffStat, GitStatusBadge};
|
|||||||
pub use blame::{Blame, BlameView, GitBlameAnnotation};
|
pub use blame::{Blame, BlameView, GitBlameAnnotation};
|
||||||
pub use changes::{ChangeAction, Changed, ChangesList, CommitInput};
|
pub use changes::{ChangeAction, Changed, ChangesList, CommitInput};
|
||||||
pub use commits::{Commit, CommitItem, CommitList};
|
pub use commits::{Commit, CommitItem, CommitList};
|
||||||
pub use diff::{DiffLine, LineKind, Stretch, diff, pairs, stat};
|
pub use diff::{DiffLine, LineKind, Stretch, diff, mark_words, pairs, stat};
|
||||||
pub use graph::{GraphRow, Half, Stroke, lanes};
|
pub use graph::{GraphRow, Half, Stroke, lanes};
|
||||||
pub use history::FileHistory;
|
pub use history::FileHistory;
|
||||||
pub use merge::{ConflictResolver, ThreeWayMerge};
|
pub use merge::{ConflictResolver, ThreeWayMerge};
|
||||||
|
|||||||
@@ -109,3 +109,67 @@ fn every_section_draws_with_its_own_words(cx: &mut TestAppContext) {
|
|||||||
let (_, cx) = cx.add_window_view(|_, _| Sorted);
|
let (_, cx) = cx.add_window_view(|_, _| Sorted);
|
||||||
settle(cx);
|
settle(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lines and hunks pressed, from stretches made elsewhere.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Picking {
|
||||||
|
lines: Vec<((usize, usize), bool)>,
|
||||||
|
hunks: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Picking {
|
||||||
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let (lines, hunks) = (cx.entity(), cx.entity());
|
||||||
|
let stretches = std::rc::Rc::new(super::diff("a\nb\n", "a\nc\n", 1));
|
||||||
|
div().w(px(480.0)).h(px(320.0)).child(
|
||||||
|
DiffViewer::from_stretches("picking", "a.txt", stretches)
|
||||||
|
.selected([(0, 2)])
|
||||||
|
.on_line(move |place, shift, _, cx| {
|
||||||
|
lines.update(cx, |picking, _| picking.lines.push((place, shift)))
|
||||||
|
})
|
||||||
|
.hunk_action("Stage", move |hunk, _, cx| {
|
||||||
|
hunks.update(cx, |picking, _| picking.hunks.push(hunk))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn changed_lines_and_hunks_answer_presses(cx: &mut TestAppContext) {
|
||||||
|
cx.update(|cx| {
|
||||||
|
Theme::init(cx);
|
||||||
|
forms::bind_keys(cx);
|
||||||
|
cx.bind_keys([KeyBinding::new("tab", FocusNext, None)]);
|
||||||
|
});
|
||||||
|
let (view, cx) = cx.add_window_view(|_, _| Picking::default());
|
||||||
|
cx.update(|window, _| window.activate_window());
|
||||||
|
settle(cx);
|
||||||
|
let press = |cx: &mut VisualTestContext, key: &str| {
|
||||||
|
cx.simulate_keystrokes(key);
|
||||||
|
settle(cx);
|
||||||
|
cx.simulate_event(KeyUpEvent {
|
||||||
|
keystroke: Keystroke::parse(key).expect("a key"),
|
||||||
|
});
|
||||||
|
settle(cx);
|
||||||
|
};
|
||||||
|
// Past the two layout segments: the hunk's button, then its lines.
|
||||||
|
let next = |cx: &mut VisualTestContext| {
|
||||||
|
cx.update(|window, cx| window.focus_next(cx));
|
||||||
|
settle(cx);
|
||||||
|
};
|
||||||
|
(0..3).for_each(|_| next(cx));
|
||||||
|
press(cx, "enter");
|
||||||
|
next(cx);
|
||||||
|
press(cx, "enter");
|
||||||
|
next(cx);
|
||||||
|
press(cx, "enter");
|
||||||
|
let (lines, hunks) = view.read_with(cx, |picking, _| {
|
||||||
|
(picking.lines.clone(), picking.hunks.clone())
|
||||||
|
});
|
||||||
|
assert_eq!(hunks, [0], "the hunk's button");
|
||||||
|
assert_eq!(
|
||||||
|
lines,
|
||||||
|
[((0, 1), false), ((0, 2), false)],
|
||||||
|
"removed, then added"
|
||||||
|
);
|
||||||
|
}
|
||||||
+200
-52
@@ -2,20 +2,21 @@ use std::{ops::Range, rc::Rc};
|
|||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, ElementId, FontWeight, HighlightStyle, Hsla, InteractiveElement, IntoElement,
|
AnyElement, App, ElementId, FontWeight, HighlightStyle, Hsla, InteractiveElement, IntoElement,
|
||||||
MouseButton, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled,
|
MouseButton, ParentElement, RenderOnce, Role, SharedString, StatefulInteractiveElement, Styled,
|
||||||
StyledText, Window, div, prelude::*, relative, transparent_black, uniform_list,
|
StyledText, Window, div, prelude::*, relative, transparent_black, uniform_list,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
badges::DiffStat,
|
badges::DiffStat,
|
||||||
diff::{DiffLine, LineKind, Stretch, diff, pairs, side_numbers},
|
diff::{DiffLine, LineKind, Stretch, diff, pair_places, side_numbers},
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
buttons::SegmentedControl,
|
buttons::{Button, ButtonVariant, SegmentedControl},
|
||||||
editor::{code_colors, stack},
|
editor::{code_colors, stack},
|
||||||
|
i18n,
|
||||||
primitives::FocusRing,
|
primitives::FocusRing,
|
||||||
theme::{ActiveTheme, ControlSize, Palette, TextSize},
|
theme::{ActiveTheme, ControlSize, Palette, TextSize},
|
||||||
typography::{LEADING, format},
|
typography::LEADING,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// How a diff lays out.
|
/// How a diff lays out.
|
||||||
@@ -28,12 +29,16 @@ pub enum DiffLayout {
|
|||||||
/// Unchanged lines kept around each change.
|
/// Unchanged lines kept around each change.
|
||||||
const CONTEXT: usize = 3;
|
const CONTEXT: usize = 3;
|
||||||
|
|
||||||
|
/// A line's place: its stretch, then its line there.
|
||||||
|
pub type Place = (usize, usize);
|
||||||
|
|
||||||
/// A row as the viewer draws it.
|
/// A row as the viewer draws it.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
enum Shown {
|
enum Shown {
|
||||||
Header(SharedString),
|
/// A hunk's header and its place among the stretches.
|
||||||
Line(DiffLine),
|
Header(usize, SharedString),
|
||||||
Pair(Option<DiffLine>, Option<DiffLine>),
|
Line(Place, DiffLine),
|
||||||
|
Pair(Option<(Place, DiffLine)>, Option<(Place, DiffLine)>),
|
||||||
/// A folded stretch: its place among the stretches and how many lines it hides.
|
/// A folded stretch: its place among the stretches and how many lines it hides.
|
||||||
Fold(usize, usize),
|
Fold(usize, usize),
|
||||||
}
|
}
|
||||||
@@ -43,20 +48,29 @@ fn shown(stretches: &[Stretch], layout: DiffLayout, open: &[usize]) -> Vec<Shown
|
|||||||
for (ix, stretch) in stretches.iter().enumerate() {
|
for (ix, stretch) in stretches.iter().enumerate() {
|
||||||
match stretch {
|
match stretch {
|
||||||
Stretch::Hunk { header, lines } => {
|
Stretch::Hunk { header, lines } => {
|
||||||
rows.push(Shown::Header(header.clone()));
|
rows.push(Shown::Header(ix, header.clone()));
|
||||||
|
let at = |line: usize| ((ix, line), lines[line].clone());
|
||||||
match layout {
|
match layout {
|
||||||
DiffLayout::Unified => rows.extend(lines.iter().cloned().map(Shown::Line)),
|
DiffLayout::Unified => rows.extend((0..lines.len()).map(|line| {
|
||||||
|
let (place, line) = at(line);
|
||||||
|
Shown::Line(place, line)
|
||||||
|
})),
|
||||||
DiffLayout::Split => rows.extend(
|
DiffLayout::Split => rows.extend(
|
||||||
pairs(lines)
|
pair_places(lines)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(left, right)| Shown::Pair(left.cloned(), right.cloned())),
|
.map(|(left, right)| Shown::Pair(left.map(at), right.map(at))),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Stretch::Folded(lines) if open.contains(&ix) => {
|
Stretch::Folded(lines) if open.contains(&ix) => {
|
||||||
rows.extend(lines.iter().map(|line| match layout {
|
rows.extend(lines.iter().enumerate().map(|(at, line)| {
|
||||||
DiffLayout::Unified => Shown::Line(line.clone()),
|
let place = (ix, at);
|
||||||
DiffLayout::Split => Shown::Pair(Some(line.clone()), Some(line.clone())),
|
match layout {
|
||||||
|
DiffLayout::Unified => Shown::Line(place, line.clone()),
|
||||||
|
DiffLayout::Split => {
|
||||||
|
Shown::Pair(Some((place, line.clone())), Some((place, line.clone())))
|
||||||
|
}
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
Stretch::Folded(lines) => rows.push(Shown::Fold(ix, lines.len())),
|
Stretch::Folded(lines) => rows.push(Shown::Fold(ix, lines.len())),
|
||||||
@@ -67,6 +81,7 @@ fn shown(stretches: &[Stretch], layout: DiffLayout, open: &[usize]) -> Vec<Shown
|
|||||||
|
|
||||||
type OnIndex = Rc<dyn Fn(usize, &mut Window, &mut App)>;
|
type OnIndex = Rc<dyn Fn(usize, &mut Window, &mut App)>;
|
||||||
type OnLayout = Rc<dyn Fn(DiffLayout, &mut Window, &mut App)>;
|
type OnLayout = Rc<dyn Fn(DiffLayout, &mut Window, &mut App)>;
|
||||||
|
type OnLine = Rc<dyn Fn(Place, bool, &mut Window, &mut App)>;
|
||||||
|
|
||||||
/// One file's changes: each change in its context with old and new numbers and its changed words marked, the stretches between folded until pressed; unified or side by side. Long diffs draw only the rows in view; it fills its box.
|
/// One file's changes: each change in its context with old and new numbers and its changed words marked, the stretches between folded until pressed; unified or side by side. Long diffs draw only the rows in view; it fills its box.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
@@ -80,6 +95,9 @@ pub struct DiffViewer {
|
|||||||
on_open: Option<OnIndex>,
|
on_open: Option<OnIndex>,
|
||||||
on_layout: Option<OnLayout>,
|
on_layout: Option<OnLayout>,
|
||||||
headless: bool,
|
headless: bool,
|
||||||
|
selected: Rc<Vec<Place>>,
|
||||||
|
on_line: Option<OnLine>,
|
||||||
|
hunk_action: Option<(SharedString, OnIndex)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiffViewer {
|
impl DiffViewer {
|
||||||
@@ -99,9 +117,62 @@ impl DiffViewer {
|
|||||||
on_open: None,
|
on_open: None,
|
||||||
on_layout: None,
|
on_layout: None,
|
||||||
headless: false,
|
headless: false,
|
||||||
|
selected: Rc::default(),
|
||||||
|
on_line: None,
|
||||||
|
hunk_action: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stretches made elsewhere, such as from Git's own diff.
|
||||||
|
pub fn from_stretches(
|
||||||
|
id: impl Into<ElementId>,
|
||||||
|
path: impl Into<SharedString>,
|
||||||
|
stretches: Rc<Vec<Stretch>>,
|
||||||
|
) -> Self {
|
||||||
|
let stat = stretches
|
||||||
|
.iter()
|
||||||
|
.filter_map(|stretch| match stretch {
|
||||||
|
Stretch::Hunk { lines, .. } => Some(lines),
|
||||||
|
Stretch::Folded(_) => None,
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
.fold((0, 0), |(added, removed), line| match line.kind {
|
||||||
|
LineKind::Added => (added + 1, removed),
|
||||||
|
LineKind::Removed => (added, removed + 1),
|
||||||
|
LineKind::Same => (added, removed),
|
||||||
|
});
|
||||||
|
Self {
|
||||||
|
stretches,
|
||||||
|
stat,
|
||||||
|
..Self::new(id, path, "", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Changed lines shown as picked.
|
||||||
|
pub fn selected(mut self, places: impl IntoIterator<Item = Place>) -> Self {
|
||||||
|
self.selected = Rc::new(places.into_iter().collect());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes changed lines pressable; true when Shift was held.
|
||||||
|
pub fn on_line(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(Place, bool, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_line = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A button on each hunk's header, given the hunk's place.
|
||||||
|
pub fn hunk_action(
|
||||||
|
mut self,
|
||||||
|
label: impl Into<SharedString>,
|
||||||
|
handler: impl Fn(usize, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.hunk_action = Some((label.into(), Rc::new(handler)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Leaves the file's header, its path, counts and layout, to the owner, as a change card draws it.
|
/// Leaves the file's header, its path, counts and layout, to the owner, as a change card draws it.
|
||||||
pub fn headless(mut self) -> Self {
|
pub fn headless(mut self) -> Self {
|
||||||
self.headless = true;
|
self.headless = true;
|
||||||
@@ -178,8 +249,8 @@ impl RenderOnce for DiffViewer {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.size(ControlSize::Sm)
|
.size(ControlSize::Sm)
|
||||||
.segment("unified", "Unified", None)
|
.segment("unified", i18n::text(cx, "git.diff.unified", &[]), None)
|
||||||
.segment("split", "Split", None);
|
.segment("split", i18n::text(cx, "git.diff.split", &[]), None);
|
||||||
let layout = match self.on_layout {
|
let layout = match self.on_layout {
|
||||||
Some(on_layout) => layout.on_change(move |key, window, cx| {
|
Some(on_layout) => layout.on_change(move |key, window, cx| {
|
||||||
let next = if key.as_ref() == "split" {
|
let next = if key.as_ref() == "split" {
|
||||||
@@ -205,6 +276,11 @@ impl RenderOnce for DiffViewer {
|
|||||||
.len()
|
.len()
|
||||||
.max(2);
|
.max(2);
|
||||||
let (id, on_open) = (self.id.clone(), self.on_open.clone());
|
let (id, on_open) = (self.id.clone(), self.on_open.clone());
|
||||||
|
let (selected, on_line, hunk_action) = (
|
||||||
|
self.selected.clone(),
|
||||||
|
self.on_line.clone(),
|
||||||
|
self.hunk_action.clone(),
|
||||||
|
);
|
||||||
let list = uniform_list(
|
let list = uniform_list(
|
||||||
(self.id.clone(), "rows"),
|
(self.id.clone(), "rows"),
|
||||||
rows.len(),
|
rows.len(),
|
||||||
@@ -218,29 +294,72 @@ impl RenderOnce for DiffViewer {
|
|||||||
.text_color(colors.fg_subtle)
|
.text_color(colors.fg_subtle)
|
||||||
.children(value.map(|value| value.to_string()))
|
.children(value.map(|value| value.to_string()))
|
||||||
};
|
};
|
||||||
let side =
|
// Changed lines toggle when the owner picks lines.
|
||||||
|line: Option<&DiffLine>, shown: Option<usize>, cx: &App| -> AnyElement {
|
let pick = |element: gpui::Div,
|
||||||
let (wash, _) = line
|
own: ElementId,
|
||||||
.map_or((Some(colors.hover.opacity(0.5)), colors.hover), |line| {
|
place: Place,
|
||||||
washes(line.kind, &colors)
|
line: &DiffLine,
|
||||||
});
|
cx: &App|
|
||||||
div()
|
-> gpui::Stateful<gpui::Div> {
|
||||||
.flex_1()
|
let picked = selected.contains(&place);
|
||||||
.min_w_0()
|
let element = element.when(picked, |element| element.bg(colors.selection));
|
||||||
.flex()
|
match (&on_line, line.kind) {
|
||||||
.gap_2()
|
(Some(on_line), LineKind::Added | LineKind::Removed) => {
|
||||||
.overflow_hidden()
|
let on_line = on_line.clone();
|
||||||
.when_some(wash, |side, wash| side.bg(wash))
|
let sign = if line.kind == LineKind::Added {
|
||||||
.child(number(shown))
|
"+"
|
||||||
.children(line.map(|line| {
|
} else {
|
||||||
div().whitespace_nowrap().child(code(line, &colors, cx))
|
"−"
|
||||||
}))
|
};
|
||||||
.into_any_element()
|
element
|
||||||
};
|
.id((id.clone(), format!("line-{}-{}", place.0, place.1)))
|
||||||
|
.role(Role::ListItem)
|
||||||
|
.aria_label(SharedString::from(format!("{sign} {}", line.text)))
|
||||||
|
.aria_selected(picked)
|
||||||
|
.tab_index(0)
|
||||||
|
.focus_ring(cx)
|
||||||
|
.cursor_pointer()
|
||||||
|
.on_mouse_down(MouseButton::Left, |_, window, _| {
|
||||||
|
window.prevent_default()
|
||||||
|
})
|
||||||
|
.on_click(move |event, window, cx| {
|
||||||
|
on_line(place, event.modifiers().shift, window, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => element.id(own),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let side = |line: Option<&(Place, DiffLine)>,
|
||||||
|
shown: Option<usize>,
|
||||||
|
at: &str,
|
||||||
|
cx: &App|
|
||||||
|
-> AnyElement {
|
||||||
|
let (wash, _) = line.map_or(
|
||||||
|
(Some(colors.hover.opacity(0.5)), colors.hover),
|
||||||
|
|(_, line)| washes(line.kind, &colors),
|
||||||
|
);
|
||||||
|
let own = ElementId::from(SharedString::from(at.to_string()));
|
||||||
|
let element = div()
|
||||||
|
.flex_1()
|
||||||
|
.min_w_0()
|
||||||
|
.flex()
|
||||||
|
.gap_2()
|
||||||
|
.overflow_hidden()
|
||||||
|
.when_some(wash, |side, wash| side.bg(wash))
|
||||||
|
.child(number(shown))
|
||||||
|
.children(line.map(|(_, line)| {
|
||||||
|
div().whitespace_nowrap().child(code(line, &colors, cx))
|
||||||
|
}));
|
||||||
|
match line {
|
||||||
|
Some((place, line)) => {
|
||||||
|
pick(element, own, *place, line, cx).into_any_element()
|
||||||
|
}
|
||||||
|
None => element.id(own).into_any_element(),
|
||||||
|
}
|
||||||
|
};
|
||||||
range
|
range
|
||||||
.map(|ix| {
|
.map(|ix| {
|
||||||
let row = div()
|
let row = div()
|
||||||
.id(ix)
|
|
||||||
.w_full()
|
.w_full()
|
||||||
.flex()
|
.flex()
|
||||||
.px_2()
|
.px_2()
|
||||||
@@ -249,20 +368,31 @@ impl RenderOnce for DiffViewer {
|
|||||||
.whitespace_nowrap()
|
.whitespace_nowrap()
|
||||||
.line_height(relative(LEADING));
|
.line_height(relative(LEADING));
|
||||||
match &rows[ix] {
|
match &rows[ix] {
|
||||||
Shown::Header(header) => row
|
Shown::Header(stretch, header) => {
|
||||||
.bg(colors.hover)
|
let action = hunk_action.clone().map(|(label, run)| {
|
||||||
.text_color(colors.fg_muted)
|
let stretch = *stretch;
|
||||||
.child(header.clone())
|
Button::new((id.clone(), format!("hunk-{stretch}")), label)
|
||||||
.into_any_element(),
|
.variant(ButtonVariant::Ghost)
|
||||||
Shown::Line(line) => {
|
.size(ControlSize::Sm)
|
||||||
|
.on_click(move |_, window, cx| run(stretch, window, cx))
|
||||||
|
});
|
||||||
|
row.id(ix)
|
||||||
|
.bg(colors.hover)
|
||||||
|
.items_center()
|
||||||
|
.text_color(colors.fg_muted)
|
||||||
|
.child(div().flex_1().min_w_0().child(header.clone()))
|
||||||
|
.children(action)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
Shown::Line(place, line) => {
|
||||||
let (wash, _) = washes(line.kind, &colors);
|
let (wash, _) = washes(line.kind, &colors);
|
||||||
let sign = match line.kind {
|
let sign = match line.kind {
|
||||||
LineKind::Same => " ",
|
LineKind::Same => " ",
|
||||||
LineKind::Added => "+",
|
LineKind::Added => "+",
|
||||||
LineKind::Removed => "−",
|
LineKind::Removed => "−",
|
||||||
};
|
};
|
||||||
row.gap_2()
|
let row = row.gap_2().when_some(wash, |row, wash| row.bg(wash));
|
||||||
.when_some(wash, |row, wash| row.bg(wash))
|
pick(row, ElementId::from(ix), *place, line, cx)
|
||||||
.child(number(line.old))
|
.child(number(line.old))
|
||||||
.child(number(line.new))
|
.child(number(line.new))
|
||||||
.child(
|
.child(
|
||||||
@@ -278,10 +408,14 @@ impl RenderOnce for DiffViewer {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
Shown::Pair(left, right) => {
|
Shown::Pair(left, right) => {
|
||||||
let (old, new) = side_numbers(left.as_ref(), right.as_ref());
|
let (old, new) = side_numbers(
|
||||||
row.gap_1()
|
left.as_ref().map(|(_, line)| line),
|
||||||
.child(side(left.as_ref(), old, cx))
|
right.as_ref().map(|(_, line)| line),
|
||||||
.child(side(right.as_ref(), new, cx))
|
);
|
||||||
|
row.id(ix)
|
||||||
|
.gap_1()
|
||||||
|
.child(side(left.as_ref(), old, &format!("left-{ix}"), cx))
|
||||||
|
.child(side(right.as_ref(), new, &format!("right-{ix}"), cx))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
Shown::Fold(stretch, count) => {
|
Shown::Fold(stretch, count) => {
|
||||||
@@ -304,10 +438,14 @@ impl RenderOnce for DiffViewer {
|
|||||||
})
|
})
|
||||||
.child(format!(
|
.child(format!(
|
||||||
"⋯ {}",
|
"⋯ {}",
|
||||||
format::plural(
|
i18n::text(
|
||||||
*count as u64,
|
cx,
|
||||||
"unchanged line",
|
if *count == 1 {
|
||||||
"unchanged lines"
|
"git.diff.unchanged.one"
|
||||||
|
} else {
|
||||||
|
"git.diff.unchanged.other"
|
||||||
|
},
|
||||||
|
&[("n", &count.to_string())],
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
@@ -355,3 +493,13 @@ impl RenderOnce for DiffViewer {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn stretches_made_elsewhere_count_their_lines() {
|
||||||
|
let stretches = std::rc::Rc::new(super::diff("a\nb\nc\n", "a\nx\ny\nc\n", 1));
|
||||||
|
let viewer = super::DiffViewer::from_stretches("counted", "a.txt", stretches);
|
||||||
|
assert_eq!(viewer.stat, (2, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,6 +78,10 @@ const EN: &[(&str, &str)] = &[
|
|||||||
("git.changes.unstage", "Unstage"),
|
("git.changes.unstage", "Unstage"),
|
||||||
("git.changes.stage_all", "Stage all"),
|
("git.changes.stage_all", "Stage all"),
|
||||||
("git.changes.unstage_all", "Unstage all"),
|
("git.changes.unstage_all", "Unstage all"),
|
||||||
|
("git.diff.unified", "Unified"),
|
||||||
|
("git.diff.split", "Split"),
|
||||||
|
("git.diff.unchanged.one", "{n} unchanged line"),
|
||||||
|
("git.diff.unchanged.other", "{n} unchanged lines"),
|
||||||
];
|
];
|
||||||
|
|
||||||
const ZH_CN: &[(&str, &str)] = &[
|
const ZH_CN: &[(&str, &str)] = &[
|
||||||
@@ -154,6 +158,10 @@ const ZH_CN: &[(&str, &str)] = &[
|
|||||||
("git.changes.unstage", "取消暂存"),
|
("git.changes.unstage", "取消暂存"),
|
||||||
("git.changes.stage_all", "全部暂存"),
|
("git.changes.stage_all", "全部暂存"),
|
||||||
("git.changes.unstage_all", "全部取消暂存"),
|
("git.changes.unstage_all", "全部取消暂存"),
|
||||||
|
("git.diff.unified", "行内"),
|
||||||
|
("git.diff.split", "并排"),
|
||||||
|
("git.diff.unchanged.one", "{n} 行未改动"),
|
||||||
|
("git.diff.unchanged.other", "{n} 行未改动"),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Ely's own message for `key` in `locale`, English for a locale Ely does not ship; fails on an unknown key.
|
/// Ely's own message for `key` in `locale`, English for a locale Ely does not ship; fails on an unknown key.
|
||||||
|
|||||||
Reference in new issue
Block a user