mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 08:58:21 +00:00
Add scrolling to commit message in details
- Manually wrap commit message using `textwrap` Closes #181
This commit is contained in:
parent
0e81f062da
commit
1a90fd365f
9 changed files with 299 additions and 34 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -354,5 +354,6 @@ impl Component for FileTreeComponent {
|
|||
}
|
||||
fn focus(&mut self, focus: bool) {
|
||||
self.focused = focus;
|
||||
self.show_selection(focus);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue