From dcb4711e4efe50359f511621480651a56589c8c0 Mon Sep 17 00:00:00 2001 From: Michael Zimmermann Date: Tue, 5 Jul 2022 17:13:12 +0200 Subject: [PATCH 1/3] move `trim_offset` to `string_utils` the diff viewer will need it in a future commit --- src/string_utils.rs | 19 +++++++++++++++++++ src/ui/reflow.rs | 18 +----------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/string_utils.rs b/src/string_utils.rs index 065f5554c5..c2cb6808a4 100644 --- a/src/string_utils.rs +++ b/src/string_utils.rs @@ -1,3 +1,6 @@ +use unicode_segmentation::UnicodeSegmentation; +use unicode_width::UnicodeWidthStr; + /// pub fn trim_length_left(s: &str, width: usize) -> &str { let len = s.len(); @@ -21,6 +24,22 @@ pub fn tabs_to_spaces(input: String) -> String { } } +/// This function will return a str slice which start at specified offset. +/// As src is a unicode str, start offset has to be calculated with each character. +pub fn trim_offset(src: &str, mut offset: usize) -> &str { + let mut start = 0; + for c in UnicodeSegmentation::graphemes(src, true) { + let w = c.width(); + if w <= offset { + offset -= w; + start += c.len(); + } else { + break; + } + } + &src[start..] +} + #[cfg(test)] mod test { use pretty_assertions::assert_eq; diff --git a/src/ui/reflow.rs b/src/ui/reflow.rs index b2417867a5..0984597c75 100644 --- a/src/ui/reflow.rs +++ b/src/ui/reflow.rs @@ -1,6 +1,6 @@ +use crate::string_utils::trim_offset; use easy_cast::Cast; use tui::text::StyledGrapheme; -use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; const NBSP: &str = "\u{00a0}"; @@ -233,22 +233,6 @@ impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> { } } -/// This function will return a str slice which start at specified offset. -/// As src is a unicode str, start offset has to be calculated with each character. -fn trim_offset(src: &str, mut offset: usize) -> &str { - let mut start = 0; - for c in UnicodeSegmentation::graphemes(src, true) { - let w = c.width(); - if w <= offset { - offset -= w; - start += c.len(); - } else { - break; - } - } - &src[start..] -} - #[cfg(test)] mod test { use super::*; From 4ce1948ac572a8520f438bbbed0d52d1667f03eb Mon Sep 17 00:00:00 2001 From: Michael Zimmermann Date: Thu, 7 Jul 2022 17:57:06 +0200 Subject: [PATCH 2/3] ui: scrollbar: add support for horizontal scrolling --- src/components/blame_file.rs | 1 + src/components/commitlist.rs | 3 +- src/components/file_revlog.rs | 3 +- src/components/syntax_text.rs | 1 + src/components/taglist.rs | 1 + src/components/utils/scroll_vertical.rs | 3 +- src/ui/mod.rs | 2 +- src/ui/scrollbar.rs | 74 +++++++++++++++++++++++-- 8 files changed, 78 insertions(+), 10 deletions(-) diff --git a/src/components/blame_file.rs b/src/components/blame_file.rs index 7411c29704..25fa96fce5 100644 --- a/src/components/blame_file.rs +++ b/src/components/blame_file.rs @@ -123,6 +123,7 @@ impl DrawableComponent for BlameFileComponent { // // https://github.com/fdehau/tui-rs/issues/448 table_state.selected().unwrap_or(0), + ui::Orientation::Vertical, ); self.table_state.set(table_state); diff --git a/src/components/commitlist.rs b/src/components/commitlist.rs index 7ef6cc0ebf..cf4a36d48b 100644 --- a/src/components/commitlist.rs +++ b/src/components/commitlist.rs @@ -7,7 +7,7 @@ use crate::{ keys::SharedKeyConfig, strings::{self, symbol}, ui::style::{SharedTheme, Theme}, - ui::{calc_scroll_top, draw_scrollbar}, + ui::{calc_scroll_top, draw_scrollbar, Orientation}, }; use anyhow::Result; use asyncgit::sync::{CommitId, Tags}; @@ -419,6 +419,7 @@ impl DrawableComponent for CommitList { &self.theme, self.count_total, self.selection, + Orientation::Vertical, ); Ok(()) diff --git a/src/components/file_revlog.rs b/src/components/file_revlog.rs index da38386563..a0a5634d6d 100644 --- a/src/components/file_revlog.rs +++ b/src/components/file_revlog.rs @@ -9,7 +9,7 @@ use crate::{ keys::SharedKeyConfig, queue::{InternalEvent, NeedsUpdate, Queue}, strings, - ui::{draw_scrollbar, style::SharedTheme}, + ui::{draw_scrollbar, style::SharedTheme, Orientation}, }; use anyhow::Result; use asyncgit::{ @@ -410,6 +410,7 @@ impl FileRevlogComponent { &self.theme, self.count_total, table_state.selected().unwrap_or(0), + Orientation::Vertical, ); self.table_state.set(table_state); diff --git a/src/components/syntax_text.rs b/src/components/syntax_text.rs index f1f4a24b05..7eb031c211 100644 --- a/src/components/syntax_text.rs +++ b/src/components/syntax_text.rs @@ -239,6 +239,7 @@ impl DrawableComponent for SyntaxTextComponent { state.height().saturating_sub(2), )), usize::from(state.scroll().y), + ui::Orientation::Vertical, ); } diff --git a/src/components/taglist.rs b/src/components/taglist.rs index e908c1becb..4829b68d63 100644 --- a/src/components/taglist.rs +++ b/src/components/taglist.rs @@ -128,6 +128,7 @@ impl DrawableComponent for TagListComponent { &self.theme, number_of_rows, table_state.selected().unwrap_or(0), + ui::Orientation::Vertical, ); self.table_state.set(table_state); diff --git a/src/components/utils/scroll_vertical.rs b/src/components/utils/scroll_vertical.rs index 54fa7e1bb9..a8a62a0697 100644 --- a/src/components/utils/scroll_vertical.rs +++ b/src/components/utils/scroll_vertical.rs @@ -4,7 +4,7 @@ use tui::{backend::Backend, layout::Rect, Frame}; use crate::{ components::ScrollType, - ui::{draw_scrollbar, style::SharedTheme}, + ui::{draw_scrollbar, style::SharedTheme, Orientation}, }; pub struct VerticalScroll { @@ -95,6 +95,7 @@ impl VerticalScroll { theme, self.max_top.get(), self.top.get(), + Orientation::Vertical, ); } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 1e8d3c4797..683b904e0e 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -6,7 +6,7 @@ pub mod style; mod syntax_text; use filetreelist::MoveSelection; -pub use scrollbar::draw_scrollbar; +pub use scrollbar::{draw_scrollbar, Orientation}; pub use scrolllist::{draw_list, draw_list_block}; pub use stateful_paragraph::{ ParagraphState, ScrollPos, StatefulParagraph, diff --git a/src/ui/scrollbar.rs b/src/ui/scrollbar.rs index adf4dec1d9..c0580c2661 100644 --- a/src/ui/scrollbar.rs +++ b/src/ui/scrollbar.rs @@ -6,32 +6,40 @@ use tui::{ buffer::Buffer, layout::{Margin, Rect}, style::Style, - symbols::{block::FULL, line::DOUBLE_VERTICAL}, + symbols::{ + block::FULL, + line::{DOUBLE_HORIZONTAL, DOUBLE_VERTICAL}, + }, widgets::Widget, Frame, }; +pub enum Orientation { + Vertical, + Horizontal, +} + /// struct Scrollbar { max: u16, pos: u16, style_bar: Style, style_pos: Style, + orientation: Orientation, } impl Scrollbar { - fn new(max: usize, pos: usize) -> Self { + fn new(max: usize, pos: usize, orientation: Orientation) -> Self { Self { max: u16::try_from(max).unwrap_or_default(), pos: u16::try_from(pos).unwrap_or_default(), style_pos: Style::default(), style_bar: Style::default(), + orientation, } } -} -impl Widget for Scrollbar { - fn render(self, area: Rect, buf: &mut Buffer) { + fn render_vertical(self, area: Rect, buf: &mut Buffer) { if area.height <= 2 { return; } @@ -67,6 +75,59 @@ impl Widget for Scrollbar { buf.set_string(right, bar_top + pos, FULL, self.style_pos); } + + fn render_horizontal(self, area: Rect, buf: &mut Buffer) { + if area.width <= 2 { + return; + } + + if self.max == 0 { + return; + } + + let bottom = area.bottom().saturating_sub(1); + if bottom <= area.top() { + return; + }; + + let (bar_left, bar_width) = { + let scrollbar_area = area.inner(&Margin { + horizontal: 1, + vertical: 0, + }); + + (scrollbar_area.left(), scrollbar_area.width) + }; + + for x in bar_left..(bar_left + bar_width) { + buf.set_string( + x, + bottom, + DOUBLE_HORIZONTAL, + self.style_bar, + ); + } + + let progress = f32::from(self.pos) / f32::from(self.max); + let progress = if progress > 1.0 { 1.0 } else { progress }; + let pos = f32::from(bar_width) * progress; + + let pos: u16 = pos.cast_nearest(); + let pos = pos.saturating_sub(1); + + buf.set_string(bar_left + pos, bottom, FULL, self.style_pos); + } +} + +impl Widget for Scrollbar { + fn render(self, area: Rect, buf: &mut Buffer) { + match &self.orientation { + Orientation::Vertical => self.render_vertical(area, buf), + Orientation::Horizontal => { + self.render_horizontal(area, buf) + } + } + } } pub fn draw_scrollbar( @@ -75,8 +136,9 @@ pub fn draw_scrollbar( theme: &SharedTheme, max: usize, pos: usize, + orientation: Orientation, ) { - let mut widget = Scrollbar::new(max, pos); + let mut widget = Scrollbar::new(max, pos, orientation); widget.style_pos = theme.scroll_bar_pos(); f.render_widget(widget, r); } From fbb0fe1a7addbd6ad79f1638aecc3062f780d74e Mon Sep 17 00:00:00 2001 From: Michael Zimmermann Date: Tue, 5 Jul 2022 17:14:14 +0200 Subject: [PATCH 3/3] diff: implement horizontal scrolling converting tabs to spaces had to be moved from after the formatting to before it because `trim_offset` seems to remove leading tabs from the output string (not the input string) for some reason. calculating the longtest line is cached to prevent a theoretical decrease in performance. --- CHANGELOG.md | 1 + src/components/diff.rs | 65 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d80b02472..25199f3ff9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * switch focus to index after staging last file ([#1169](https://github.com/extrawurst/gitui/pull/1169)) * fix stashlist multi marking not updated after dropping ([#1207](https://github.com/extrawurst/gitui/pull/1207)) * 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)) +* support horizontal scrolling in diff view ([#1017](https://github.com/extrawurst/gitui/issues/1017)) ## [0.20.1] - 2021-01-26 diff --git a/src/components/diff.rs b/src/components/diff.rs index a485421338..c3486b30b6 100644 --- a/src/components/diff.rs +++ b/src/components/diff.rs @@ -7,8 +7,9 @@ use crate::{ keys::SharedKeyConfig, queue::{Action, InternalEvent, NeedsUpdate, Queue, ResetItem}, string_utils::tabs_to_spaces, + string_utils::trim_offset, strings, try_or_popup, - ui::style::SharedTheme, + ui::{draw_scrollbar, style::SharedTheme, Orientation}, }; use anyhow::Result; use asyncgit::{ @@ -102,6 +103,7 @@ impl Selection { pub struct DiffComponent { repo: RepoPathRef, diff: Option, + longest_line: usize, pending: bool, selection: Selection, selected_hunk: Option, @@ -113,6 +115,7 @@ pub struct DiffComponent { theme: SharedTheme, key_config: SharedKeyConfig, is_immutable: bool, + scrolled_right: usize, } impl DiffComponent { @@ -131,6 +134,7 @@ impl DiffComponent { pending: false, selected_hunk: None, diff: None, + longest_line: 0, current_size: Cell::new((0, 0)), selection: Selection::Single(0), scroll: VerticalScroll::new(), @@ -138,6 +142,7 @@ impl DiffComponent { key_config, is_immutable, repo, + scrolled_right: 0, } } /// @@ -155,10 +160,12 @@ impl DiffComponent { pub fn clear(&mut self, pending: bool) { self.current = Current::default(); self.diff = None; + self.longest_line = 0; self.scroll.reset(); self.selection = Selection::Single(0); self.selected_hunk = None; self.pending = pending; + self.scrolled_right = 0; } /// pub fn update( @@ -182,6 +189,24 @@ impl DiffComponent { self.diff = Some(diff); + self.longest_line = self + .diff + .as_ref() + .map(|diff| { + diff.hunks + .iter() + .map(|hunk| { + hunk.lines + .iter() + .map(|line| line.content.len()) + .max() + .unwrap_or(0) + }) + .max() + .unwrap_or(0) + }) + .unwrap_or(0); + if reset_selection { self.scroll.reset(); self.selection = Selection::Single(0); @@ -241,6 +266,11 @@ impl DiffComponent { self.diff.as_ref().map_or(0, |diff| diff.lines) } + fn max_scroll_right(&self) -> usize { + self.longest_line + .saturating_sub(self.current_size.get().0.into()) + } + fn modify_selection(&mut self, direction: Direction) { if self.diff.is_some() { self.selection.modify(direction, self.lines_count()); @@ -379,6 +409,7 @@ impl DiffComponent { hunk_selected, i == hunk_len as usize - 1, &self.theme, + self.scrolled_right, )); lines_added += 1; } @@ -401,6 +432,7 @@ impl DiffComponent { selected_hunk: bool, end_of_hunk: bool, theme: &SharedTheme, + scrolled_right: usize, ) -> Spans<'a> { let style = theme.diff_hunk_marker(selected_hunk); @@ -419,18 +451,22 @@ impl DiffComponent { } }; + let content = + tabs_to_spaces(line.content.as_ref().to_string()); + let content = trim_offset(&content, scrolled_right); + let filled = if selected { // selected line - format!("{:w$}\n", line.content, w = width as usize) + format!("{:w$}\n", content, w = width as usize) } else { // weird eof missing eol line - format!("{}\n", line.content) + format!("{}\n", content) }; Spans::from(vec![ left_side_of_line, Span::styled( - Cow::from(tabs_to_spaces(filled)), + Cow::from(filled), theme.diff_line(line.line_type, selected), ), ]) @@ -645,6 +681,17 @@ impl DrawableComponent for DiffComponent { if self.focused() { self.scroll.draw(f, r, &self.theme); + + if self.scrolled_right != 0 { + draw_scrollbar( + f, + r, + &self.theme, + self.max_scroll_right(), + self.scrolled_right, + Orientation::Horizontal, + ); + } } Ok(()) @@ -749,6 +796,16 @@ impl Component for DiffComponent { } else if e == self.key_config.keys.page_down { self.move_selection(ScrollType::PageDown); Ok(EventState::Consumed) + } else if e == self.key_config.keys.move_right { + if self.scrolled_right < self.max_scroll_right() { + self.scrolled_right += 1; + } + Ok(EventState::Consumed) + } else if e == self.key_config.keys.move_left + && self.scrolled_right > 0 + { + self.scrolled_right -= 1; + Ok(EventState::Consumed) } else if e == self.key_config.keys.stage_unstage_item && !self.is_immutable {