Skip to content

Commit a55cfc2

Browse files
cruesslerextrawurst
authored andcommitted
Allow to scroll diffs horizontally (#1327)
1 parent 4c7c954 commit a55cfc2

18 files changed

+343
-70
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ Bugfix followup release - check `0.22.0` notes for more infos!
9090
* switch focus to index after staging last file ([#1169](https://github.com/extrawurst/gitui/pull/1169))
9191
* fix stashlist multi marking not updated after dropping ([#1207](https://github.com/extrawurst/gitui/pull/1207))
9292
* exact matches have a higher priority and are placed to the top of the list when fuzzily finding files ([#1183](https://github.com/extrawurst/gitui/pull/1183))
93+
* support horizontal scrolling in diff view ([#1017](https://github.com/extrawurst/gitui/issues/1017))
9394

9495
### Changed
9596
* minimum supported rust version bumped to 1.60 ([#1279](https://github.com/extrawurst/gitui/pull/1279))

src/components/blame_file.rs

+1
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ impl DrawableComponent for BlameFileComponent {
123123
//
124124
// https://github.com/fdehau/tui-rs/issues/448
125125
table_state.selected().unwrap_or(0),
126+
ui::Orientation::Vertical,
126127
);
127128

128129
self.table_state.set(table_state);

src/components/commitlist.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::{
88
queue::{InternalEvent, Queue},
99
strings::{self, symbol},
1010
ui::style::{SharedTheme, Theme},
11-
ui::{calc_scroll_top, draw_scrollbar},
11+
ui::{calc_scroll_top, draw_scrollbar, Orientation},
1212
};
1313
use anyhow::Result;
1414
use asyncgit::sync::{BranchInfo, CommitId, Tags};
@@ -501,6 +501,7 @@ impl DrawableComponent for CommitList {
501501
&self.theme,
502502
self.count_total,
503503
self.selection,
504+
Orientation::Vertical,
504505
);
505506

506507
Ok(())

src/components/compare_commits.rs

+7-9
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ impl DrawableComponent for CompareCommitsComponent {
4444
) -> Result<()> {
4545
if self.is_visible() {
4646
let percentages = if self.diff.focused() {
47-
(30, 70)
47+
(0, 100)
4848
} else {
4949
(50, 50)
5050
};
@@ -121,7 +121,12 @@ impl Component for CompareCommitsComponent {
121121

122122
if let Event::Key(e) = ev {
123123
if key_match(e, self.key_config.keys.exit_popup) {
124-
self.hide_stacked(false);
124+
if self.diff.focused() {
125+
self.details.focus(true);
126+
self.diff.focus(false);
127+
} else {
128+
self.hide_stacked(false);
129+
}
125130
} else if key_match(
126131
e,
127132
self.key_config.keys.focus_right,
@@ -132,13 +137,6 @@ impl Component for CompareCommitsComponent {
132137
} else if key_match(
133138
e,
134139
self.key_config.keys.focus_left,
135-
) && self.diff.focused()
136-
{
137-
self.details.focus(true);
138-
self.diff.focus(false);
139-
} else if key_match(
140-
e,
141-
self.key_config.keys.focus_left,
142140
) {
143141
self.hide_stacked(false);
144142
}

src/components/diff.rs

+72-11
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
use super::{
2+
utils::scroll_horizontal::HorizontalScroll,
23
utils::scroll_vertical::VerticalScroll, CommandBlocking,
3-
Direction, DrawableComponent, ScrollType,
4+
Direction, DrawableComponent, HorizontalScrollType, ScrollType,
45
};
56
use crate::{
67
components::{CommandInfo, Component, EventState},
78
keys::{key_match, SharedKeyConfig},
89
queue::{Action, InternalEvent, NeedsUpdate, Queue, ResetItem},
910
string_utils::tabs_to_spaces,
11+
string_utils::trim_offset,
1012
strings, try_or_popup,
1113
ui::style::SharedTheme,
1214
};
@@ -102,13 +104,15 @@ impl Selection {
102104
pub struct DiffComponent {
103105
repo: RepoPathRef,
104106
diff: Option<FileDiff>,
107+
longest_line: usize,
105108
pending: bool,
106109
selection: Selection,
107110
selected_hunk: Option<usize>,
108111
current_size: Cell<(u16, u16)>,
109112
focused: bool,
110113
current: Current,
111-
scroll: VerticalScroll,
114+
vertical_scroll: VerticalScroll,
115+
horizontal_scroll: HorizontalScroll,
112116
queue: Queue,
113117
theme: SharedTheme,
114118
key_config: SharedKeyConfig,
@@ -131,9 +135,11 @@ impl DiffComponent {
131135
pending: false,
132136
selected_hunk: None,
133137
diff: None,
138+
longest_line: 0,
134139
current_size: Cell::new((0, 0)),
135140
selection: Selection::Single(0),
136-
scroll: VerticalScroll::new(),
141+
vertical_scroll: VerticalScroll::new(),
142+
horizontal_scroll: HorizontalScroll::new(),
137143
theme,
138144
key_config,
139145
is_immutable,
@@ -155,7 +161,9 @@ impl DiffComponent {
155161
pub fn clear(&mut self, pending: bool) {
156162
self.current = Current::default();
157163
self.diff = None;
158-
self.scroll.reset();
164+
self.longest_line = 0;
165+
self.vertical_scroll.reset();
166+
self.horizontal_scroll.reset();
159167
self.selection = Selection::Single(0);
160168
self.selected_hunk = None;
161169
self.pending = pending;
@@ -182,8 +190,27 @@ impl DiffComponent {
182190

183191
self.diff = Some(diff);
184192

193+
self.longest_line = self
194+
.diff
195+
.iter()
196+
.flat_map(|diff| diff.hunks.iter())
197+
.flat_map(|hunk| hunk.lines.iter())
198+
.map(|line| {
199+
let converted_content = tabs_to_spaces(
200+
line.content.as_ref().to_string(),
201+
);
202+
203+
converted_content.len()
204+
})
205+
.max()
206+
.map_or(0, |len| {
207+
// Each hunk uses a 1-character wide vertical bar to its left to indicate
208+
// selection.
209+
len + 1
210+
});
211+
185212
if reset_selection {
186-
self.scroll.reset();
213+
self.vertical_scroll.reset();
187214
self.selection = Selection::Single(0);
188215
self.update_selection(0);
189216
} else {
@@ -241,6 +268,11 @@ impl DiffComponent {
241268
self.diff.as_ref().map_or(0, |diff| diff.lines)
242269
}
243270

271+
fn max_scroll_right(&self) -> usize {
272+
self.longest_line
273+
.saturating_sub(self.current_size.get().0.into())
274+
}
275+
244276
fn modify_selection(&mut self, direction: Direction) {
245277
if self.diff.is_some() {
246278
self.selection.modify(direction, self.lines_count());
@@ -340,7 +372,7 @@ impl DiffComponent {
340372
Span::raw(Cow::from(")")),
341373
])]);
342374
} else {
343-
let min = self.scroll.get_top();
375+
let min = self.vertical_scroll.get_top();
344376
let max = min + height as usize;
345377

346378
let mut line_cursor = 0_usize;
@@ -378,6 +410,8 @@ impl DiffComponent {
378410
hunk_selected,
379411
i == hunk_len - 1,
380412
&self.theme,
413+
self.horizontal_scroll
414+
.get_right(),
381415
));
382416
lines_added += 1;
383417
}
@@ -400,6 +434,7 @@ impl DiffComponent {
400434
selected_hunk: bool,
401435
end_of_hunk: bool,
402436
theme: &SharedTheme,
437+
scrolled_right: usize,
403438
) -> Spans<'a> {
404439
let style = theme.diff_hunk_marker(selected_hunk);
405440

@@ -418,18 +453,22 @@ impl DiffComponent {
418453
}
419454
};
420455

456+
let content =
457+
tabs_to_spaces(line.content.as_ref().to_string());
458+
let content = trim_offset(&content, scrolled_right);
459+
421460
let filled = if selected {
422461
// selected line
423-
format!("{:w$}\n", line.content, w = width as usize)
462+
format!("{content:w$}\n", w = width as usize)
424463
} else {
425464
// weird eof missing eol line
426-
format!("{}\n", line.content)
465+
format!("{content}\n")
427466
};
428467

429468
Spans::from(vec![
430469
left_side_of_line,
431470
Span::styled(
432-
Cow::from(tabs_to_spaces(filled)),
471+
Cow::from(filled),
433472
theme.diff_line(line.line_type, selected),
434473
),
435474
])
@@ -606,14 +645,20 @@ impl DrawableComponent for DiffComponent {
606645
r.height.saturating_sub(2),
607646
));
608647

648+
let current_width = self.current_size.get().0;
609649
let current_height = self.current_size.get().1;
610650

611-
self.scroll.update(
651+
self.vertical_scroll.update(
612652
self.selection.get_end(),
613653
self.lines_count(),
614654
usize::from(current_height),
615655
);
616656

657+
self.horizontal_scroll.update_no_selection(
658+
self.longest_line,
659+
current_width.into(),
660+
);
661+
617662
let title = format!(
618663
"{}{}",
619664
strings::title_diff(&self.key_config),
@@ -643,7 +688,11 @@ impl DrawableComponent for DiffComponent {
643688
);
644689

645690
if self.focused() {
646-
self.scroll.draw(f, r, &self.theme);
691+
self.vertical_scroll.draw(f, r, &self.theme);
692+
693+
if self.max_scroll_right() > 0 {
694+
self.horizontal_scroll.draw(f, r, &self.theme);
695+
}
647696
}
648697

649698
Ok(())
@@ -754,6 +803,18 @@ impl Component for DiffComponent {
754803
{
755804
self.move_selection(ScrollType::PageDown);
756805
Ok(EventState::Consumed)
806+
} else if key_match(
807+
e,
808+
self.key_config.keys.move_right,
809+
) {
810+
self.horizontal_scroll
811+
.move_right(HorizontalScrollType::Right);
812+
Ok(EventState::Consumed)
813+
} else if key_match(e, self.key_config.keys.move_left)
814+
{
815+
self.horizontal_scroll
816+
.move_right(HorizontalScrollType::Left);
817+
Ok(EventState::Consumed)
757818
} else if key_match(
758819
e,
759820
self.key_config.keys.stage_unstage_item,

src/components/file_revlog.rs

+8-10
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::{
1111
keys::SharedKeyConfig,
1212
queue::{InternalEvent, NeedsUpdate, Queue},
1313
strings,
14-
ui::{draw_scrollbar, style::SharedTheme},
14+
ui::{draw_scrollbar, style::SharedTheme, Orientation},
1515
};
1616
use anyhow::Result;
1717
use asyncgit::{
@@ -412,6 +412,7 @@ impl FileRevlogComponent {
412412
&self.theme,
413413
self.count_total,
414414
table_state.selected().unwrap_or(0),
415+
Orientation::Vertical,
415416
);
416417

417418
self.table_state.set(table_state);
@@ -445,7 +446,7 @@ impl DrawableComponent for FileRevlogComponent {
445446
) -> Result<()> {
446447
if self.visible {
447448
let percentages = if self.diff.focused() {
448-
(30, 70)
449+
(0, 100)
449450
} else {
450451
(50, 50)
451452
};
@@ -485,20 +486,17 @@ impl Component for FileRevlogComponent {
485486

486487
if let Event::Key(key) = event {
487488
if key_match(key, self.key_config.keys.exit_popup) {
488-
self.hide_stacked(false);
489+
if self.diff.focused() {
490+
self.diff.focus(false);
491+
} else {
492+
self.hide_stacked(false);
493+
}
489494
} else if key_match(
490495
key,
491496
self.key_config.keys.focus_right,
492497
) && self.can_focus_diff()
493498
{
494499
self.diff.focus(true);
495-
} else if key_match(
496-
key,
497-
self.key_config.keys.focus_left,
498-
) {
499-
if self.diff.focused() {
500-
self.diff.focus(false);
501-
}
502500
} else if key_match(key, self.key_config.keys.enter) {
503501
if let Some(commit_id) = self.selected_commit() {
504502
self.hide_stacked(true);

src/components/inspect_commit.rs

+8-10
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ impl DrawableComponent for InspectCommitComponent {
7171
) -> Result<()> {
7272
if self.is_visible() {
7373
let percentages = if self.diff.focused() {
74-
(30, 70)
74+
(0, 100)
7575
} else {
7676
(50, 50)
7777
};
@@ -126,7 +126,7 @@ impl Component for InspectCommitComponent {
126126
));
127127

128128
out.push(CommandInfo::new(
129-
strings::commands::diff_focus_left(&self.key_config),
129+
strings::commands::close_popup(&self.key_config),
130130
true,
131131
self.diff.focused() || force_all,
132132
));
@@ -157,7 +157,12 @@ impl Component for InspectCommitComponent {
157157

158158
if let Event::Key(e) = ev {
159159
if key_match(e, self.key_config.keys.exit_popup) {
160-
self.hide_stacked(false);
160+
if self.diff.focused() {
161+
self.details.focus(true);
162+
self.diff.focus(false);
163+
} else {
164+
self.hide_stacked(false);
165+
}
161166
} else if key_match(
162167
e,
163168
self.key_config.keys.focus_right,
@@ -168,13 +173,6 @@ impl Component for InspectCommitComponent {
168173
} else if key_match(
169174
e,
170175
self.key_config.keys.focus_left,
171-
) && self.diff.focused()
172-
{
173-
self.details.focus(true);
174-
self.diff.focus(false);
175-
} else if key_match(
176-
e,
177-
self.key_config.keys.focus_left,
178176
) {
179177
self.hide_stacked(false);
180178
}

src/components/mod.rs

+6
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,12 @@ pub enum ScrollType {
185185
PageDown,
186186
}
187187

188+
#[derive(Copy, Clone)]
189+
pub enum HorizontalScrollType {
190+
Left,
191+
Right,
192+
}
193+
188194
#[derive(Copy, Clone)]
189195
pub enum Direction {
190196
Up,

src/components/syntax_text.rs

+1
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ impl DrawableComponent for SyntaxTextComponent {
238238
state.height().saturating_sub(2),
239239
)),
240240
usize::from(state.scroll().y),
241+
ui::Orientation::Vertical,
241242
);
242243
}
243244

0 commit comments

Comments
 (0)