Add scrolling to commit message in details

- Manually wrap commit message using `textwrap`

Closes #181
This commit is contained in:
Christoph Rüßler 2020-07-20 21:06:12 +02:00 committed by Stephan Dilly
parent 0e81f062da
commit 1a90fd365f
9 changed files with 299 additions and 34 deletions

12
Cargo.lock generated
View file

@ -171,7 +171,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10040cdf04294b565d9e0319955430099ec3813a64c952b86a41200ad714ae48"
dependencies = [
"bitflags",
"textwrap",
"textwrap 0.11.0",
"unicode-width",
]
@ -422,6 +422,7 @@ dependencies = [
"scopetime",
"serde",
"simplelog",
"textwrap 0.12.1",
"tui",
"unicode-width",
]
@ -1130,6 +1131,15 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "textwrap"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789"
dependencies = [
"unicode-width",
]
[[package]]
name = "thiserror"
version = "1.0.20"

View file

@ -39,6 +39,7 @@ ron = "0.6"
serde = "1.0"
anyhow = "1.0.32"
unicode-width = "0.1"
textwrap = "0.12"
[target.'cfg(not(windows))'.dependencies]
pprof = { version = "0.3", features = ["flamegraph"], optional = true }

View file

@ -35,6 +35,7 @@ pub struct CommitMessage {
}
impl CommitMessage {
///
pub fn from(s: &str) -> Self {
if let Some(idx) = s.find('\n') {
let (first, rest) = s.split_at(idx);

View file

@ -19,7 +19,9 @@ pub mod utils;
pub(crate) use branch::get_branch_name;
pub use commit::{amend, commit, tag};
pub use commit_details::{get_commit_details, CommitDetails};
pub use commit_details::{
get_commit_details, CommitDetails, CommitMessage,
};
pub use commit_files::get_commit_files;
pub use commits_info::{get_commits_info, CommitId, CommitInfo};
pub use diff::get_diff_commit;

View file

@ -1,24 +1,25 @@
use crate::{
components::{
dialog_paragraph, utils::time_to_string, CommandBlocking,
CommandInfo, Component, DrawableComponent,
CommandInfo, Component, DrawableComponent, ScrollType,
},
strings,
keys,
strings::{self, commands, order},
ui::style::SharedTheme,
};
use anyhow::Result;
use asyncgit::{
sync::{self, CommitDetails, CommitId},
sync::{self, CommitDetails, CommitId, CommitMessage},
CWD,
};
use crossterm::event::Event;
use itertools::Itertools;
use std::borrow::Cow;
use std::{borrow::Cow, cell::Cell};
use sync::CommitTags;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
style::Modifier,
style::{Modifier, Style},
widgets::Text,
Frame,
};
@ -27,15 +28,24 @@ pub struct DetailsComponent {
data: Option<CommitDetails>,
tags: Vec<String>,
theme: SharedTheme,
focused: bool,
current_size: Cell<(u16, u16)>,
scroll_top: Cell<usize>,
}
type WrappedCommitMessage<'a> =
(Vec<Cow<'a, str>>, Vec<Cow<'a, str>>);
impl DetailsComponent {
///
pub const fn new(theme: SharedTheme) -> Self {
pub const fn new(theme: SharedTheme, focused: bool) -> Self {
Self {
data: None,
tags: Vec::new(),
theme,
focused,
current_size: Cell::new((0, 0)),
scroll_top: Cell::new(0),
}
}
@ -52,6 +62,8 @@ impl DetailsComponent {
None
};
self.scroll_top.set(0);
if let Some(tags) = tags {
self.tags.extend(tags)
}
@ -59,27 +71,80 @@ impl DetailsComponent {
Ok(())
}
fn get_text_message(&self) -> Vec<Text> {
fn wrap_commit_details(
message: &CommitMessage,
width: usize,
) -> WrappedCommitMessage<'_> {
let wrapped_title = textwrap::wrap(&message.subject, width);
if let Some(ref body) = message.body {
let wrapped_message: Vec<Cow<'_, str>> =
textwrap::wrap(body, width)
.into_iter()
.skip(1)
.collect();
(wrapped_title, wrapped_message)
} else {
(wrapped_title, vec![])
}
}
fn get_wrapped_lines(
&self,
width: usize,
) -> WrappedCommitMessage<'_> {
if let Some(ref data) = self.data {
if let Some(ref message) = data.message {
let mut res = vec![Text::Styled(
Cow::from(message.subject.clone()),
self.theme
.text(true, false)
.modifier(Modifier::BOLD),
)];
if let Some(ref body) = message.body {
res.push(Text::Styled(
Cow::from(body),
self.theme.text(true, false),
));
}
return res;
return Self::wrap_commit_details(message, width);
}
}
vec![]
(vec![], vec![])
}
fn get_number_of_lines(&self, width: usize) -> usize {
let (wrapped_title, wrapped_message) =
self.get_wrapped_lines(width);
wrapped_title.len() + wrapped_message.len()
}
fn get_theme_for_line(&self, bold: bool) -> Style {
if bold {
self.theme.text(true, false).modifier(Modifier::BOLD)
} else {
self.theme.text(true, false)
}
}
fn get_wrapped_text_message(
&self,
width: usize,
height: usize,
) -> Vec<Text> {
let newline = Text::Styled(
String::from("\n").into(),
self.theme.text(true, false),
);
let (wrapped_title, wrapped_message) =
self.get_wrapped_lines(width);
[&wrapped_title[..], &wrapped_message[..]]
.concat()
.iter()
.enumerate()
.skip(self.scroll_top.get())
.take(height)
.map(|(i, line)| {
Text::Styled(
line.clone(),
self.get_theme_for_line(i < wrapped_title.len()),
)
})
.intersperse(newline)
.collect()
}
fn get_text_info(&self) -> Vec<Text> {
@ -181,6 +246,38 @@ impl DetailsComponent {
vec![]
}
}
fn move_scroll_top(
&mut self,
move_type: ScrollType,
) -> Result<bool> {
if self.data.is_some() {
let old = self.scroll_top.get();
let width = self.current_size.get().0 as usize;
let height = self.current_size.get().1 as usize;
let number_of_lines = self.get_number_of_lines(width);
let max = number_of_lines.saturating_sub(height) as usize;
let new_scroll_top = match move_type {
ScrollType::Down => old.saturating_add(1),
ScrollType::Up => old.saturating_sub(1),
ScrollType::Home => 0,
ScrollType::End => max,
_ => old,
};
if new_scroll_top > max {
return Ok(false);
}
self.scroll_top.set(new_scroll_top);
return Ok(true);
}
Ok(false)
}
}
impl DrawableComponent for DetailsComponent {
@ -206,14 +303,27 @@ impl DrawableComponent for DetailsComponent {
chunks[0],
);
// We have to take the border into account which is one character on
// each side.
let border_width: u16 = 2;
let width = chunks[1].width.saturating_sub(border_width);
let height = chunks[1].height.saturating_sub(border_width);
self.current_size.set((width, height));
let wrapped_lines = self.get_wrapped_text_message(
width as usize,
height as usize,
);
f.render_widget(
dialog_paragraph(
strings::commit::DETAILS_MESSAGE_TITLE,
self.get_text_message().iter(),
wrapped_lines.iter(),
&self.theme,
false,
)
.wrap(true),
self.focused,
),
chunks[1],
);
@ -224,14 +334,122 @@ impl DrawableComponent for DetailsComponent {
impl Component for DetailsComponent {
fn commands(
&self,
_out: &mut Vec<CommandInfo>,
_force_all: bool,
out: &mut Vec<CommandInfo>,
force_all: bool,
) -> CommandBlocking {
// visibility_blocking(self)
let width = self.current_size.get().0 as usize;
let number_of_lines = self.get_number_of_lines(width);
out.push(
CommandInfo::new(
commands::NAVIGATE_COMMIT_MESSAGE,
number_of_lines > 0,
self.focused || force_all,
)
.order(order::NAV),
);
CommandBlocking::PassingOn
}
fn event(&mut self, _ev: Event) -> Result<bool> {
fn event(&mut self, event: Event) -> Result<bool> {
if self.focused {
if let Event::Key(e) = event {
return match e {
keys::MOVE_UP => {
self.move_scroll_top(ScrollType::Up)
}
keys::MOVE_DOWN => {
self.move_scroll_top(ScrollType::Down)
}
keys::HOME | keys::SHIFT_UP => {
self.move_scroll_top(ScrollType::Home)
}
keys::END | keys::SHIFT_DOWN => {
self.move_scroll_top(ScrollType::End)
}
_ => Ok(false),
};
}
}
Ok(false)
}
fn focused(&self) -> bool {
self.focused
}
fn focus(&mut self, focus: bool) {
if focus {
let width = self.current_size.get().0 as usize;
let height = self.current_size.get().1 as usize;
self.scroll_top.set(
self.get_number_of_lines(width)
.saturating_sub(height),
);
}
self.focused = focus;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn get_wrapped_lines(
message: &CommitMessage,
width: usize,
) -> Vec<Cow<'_, str>> {
let (wrapped_title, wrapped_message) =
DetailsComponent::wrap_commit_details(&message, width);
[&wrapped_title[..], &wrapped_message[..]].concat()
}
#[test]
fn test_textwrap() {
let message = CommitMessage::from("Commit message");
assert_eq!(
get_wrapped_lines(&message, 7),
vec!["Commit", "message"]
);
assert_eq!(
get_wrapped_lines(&message, 14),
vec!["Commit message"]
);
let message_with_newline =
CommitMessage::from("Commit message\n");
assert_eq!(
get_wrapped_lines(&message_with_newline, 7),
vec!["Commit", "message"]
);
assert_eq!(
get_wrapped_lines(&message_with_newline, 14),
vec!["Commit message"]
);
let message_with_body = CommitMessage::from(
"Commit message\n\nFirst line\nSecond line",
);
assert_eq!(
get_wrapped_lines(&message_with_body, 7),
vec![
"Commit", "message", "", "First", "line", "Second",
"line"
]
);
assert_eq!(
get_wrapped_lines(&message_with_body, 14),
vec!["Commit message", "", "First line", "Second line"]
);
}
}

View file

@ -5,7 +5,7 @@ use super::{
Component, DrawableComponent, FileTreeComponent,
};
use crate::{
accessors, queue::Queue, strings, ui::style::SharedTheme,
accessors, keys, queue::Queue, strings, ui::style::SharedTheme,
};
use anyhow::Result;
use asyncgit::{
@ -38,7 +38,7 @@ impl CommitDetailsComponent {
theme: SharedTheme,
) -> Self {
Self {
details: DetailsComponent::new(theme.clone()),
details: DetailsComponent::new(theme.clone(), false),
git_commit_files: AsyncCommitFiles::new(sender),
file_tree: FileTreeComponent::new(
"",
@ -146,6 +146,28 @@ impl Component for CommitDetailsComponent {
return Ok(true);
}
if self.focused() {
if let Event::Key(e) = ev {
return match e {
keys::FOCUS_BELOW if (self.details.focused()) => {
self.details.focus(false);
self.file_tree.focus(true);
return Ok(true);
}
keys::FOCUS_ABOVE
if (self.file_tree.focused()) =>
{
self.file_tree.focus(false);
self.details.focus(true);
return Ok(true);
}
_ => Ok(false),
};
}
}
Ok(false)
}
@ -164,6 +186,7 @@ impl Component for CommitDetailsComponent {
self.details.focused() || self.file_tree.focused()
}
fn focus(&mut self, focus: bool) {
self.details.focus(false);
self.file_tree.focus(focus);
self.file_tree.show_selection(true);
}

View file

@ -354,5 +354,6 @@ impl Component for FileTreeComponent {
}
fn focus(&mut self, focus: bool) {
self.focused = focus;
self.show_selection(focus);
}
}

View file

@ -27,6 +27,8 @@ pub const FOCUS_WORKDIR: KeyEvent = no_mod(KeyCode::Char('w'));
pub const FOCUS_STAGE: KeyEvent = no_mod(KeyCode::Char('s'));
pub const FOCUS_RIGHT: KeyEvent = no_mod(KeyCode::Right);
pub const FOCUS_LEFT: KeyEvent = no_mod(KeyCode::Left);
pub const FOCUS_ABOVE: KeyEvent = no_mod(KeyCode::Up);
pub const FOCUS_BELOW: KeyEvent = no_mod(KeyCode::Down);
pub const EXIT: KeyEvent =
with_mod(KeyCode::Char('c'), KeyModifiers::CONTROL);
pub const EXIT_POPUP: KeyEvent = no_mod(KeyCode::Esc);

View file

@ -86,6 +86,13 @@ pub mod commands {
CMD_GROUP_GENERAL,
);
///
pub static NAVIGATE_COMMIT_MESSAGE: CommandText =
CommandText::new(
"Nav [\u{2191}\u{2193}]",
"navigate commit message",
CMD_GROUP_GENERAL,
);
///
pub static NAVIGATE_TREE: CommandText = CommandText::new(
"Nav [\u{2190}\u{2191}\u{2192}\u{2193}]",
"navigate tree view",