add syntax highlighting for blame view (#745)

This commit is contained in:
Trung Tran 2023-10-03 23:48:50 +07:00 committed by extrawurst
parent ab95b98ef8
commit af9da95178
3 changed files with 231 additions and 57 deletions

View File

@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased ## Unreleased
### Changes
- add syntax highlighting for blame view [[@tdtrung17693](https://github.com/tdtrung17693)] ([#745](https://github.com/extrawurst/gitui/issues/745))
** multiline text editor ** ** multiline text editor **

View File

@ -399,7 +399,6 @@ impl App {
self.status_tab.update_git(ev)?; self.status_tab.update_git(ev)?;
self.stashing_tab.update_git(ev)?; self.stashing_tab.update_git(ev)?;
self.revlog.update_git(ev)?; self.revlog.update_git(ev)?;
self.blame_file_popup.update_git(ev)?;
self.file_revlog_popup.update_git(ev)?; self.file_revlog_popup.update_git(ev)?;
self.inspect_commit_popup.update_git(ev)?; self.inspect_commit_popup.update_git(ev)?;
self.compare_commits_popup.update_git(ev)?; self.compare_commits_popup.update_git(ev)?;
@ -411,6 +410,7 @@ impl App {
} }
self.files_tab.update_async(ev)?; self.files_tab.update_async(ev)?;
self.blame_file_popup.update_async(ev)?;
self.revision_files_popup.update(ev)?; self.revision_files_popup.update(ev)?;
self.tags_popup.update(ev); self.tags_popup.update(ev);

View File

@ -10,28 +10,73 @@ use crate::{
queue::{InternalEvent, Queue, StackablePopupOpen}, queue::{InternalEvent, Queue, StackablePopupOpen},
string_utils::tabs_to_spaces, string_utils::tabs_to_spaces,
strings, strings,
ui::{self, style::SharedTheme}, ui::{self, style::SharedTheme, AsyncSyntaxJob, SyntaxText},
AsyncAppNotification, AsyncNotification, SyntaxHighlightProgress,
}; };
use anyhow::Result; use anyhow::Result;
use asyncgit::{ use asyncgit::{
sync::{BlameHunk, CommitId, FileBlame}, asyncjob::AsyncSingleJob,
sync::{BlameHunk, CommitId, FileBlame, RepoPathRef},
AsyncBlame, AsyncGitNotification, BlameParams, AsyncBlame, AsyncGitNotification, BlameParams,
}; };
use crossbeam_channel::Sender;
use crossterm::event::Event; use crossterm::event::Event;
use ratatui::{ use ratatui::{
backend::Backend, backend::Backend,
layout::{Constraint, Rect}, layout::{Constraint, Rect},
symbols::line::VERTICAL, symbols::line::VERTICAL,
text::Span, text::{Span, Text},
widgets::{Block, Borders, Cell, Clear, Row, Table, TableState}, widgets::{Block, Borders, Cell, Clear, Row, Table, TableState},
Frame, Frame,
}; };
use std::{convert::TryInto, path::Path};
static NO_COMMIT_ID: &str = "0000000"; static NO_COMMIT_ID: &str = "0000000";
static NO_AUTHOR: &str = "<no author>"; static NO_AUTHOR: &str = "<no author>";
static MIN_AUTHOR_WIDTH: usize = 3; static MIN_AUTHOR_WIDTH: usize = 3;
static MAX_AUTHOR_WIDTH: usize = 20; static MAX_AUTHOR_WIDTH: usize = 20;
struct SyntaxFileBlame {
pub file_blame: FileBlame,
pub styled_text: Option<SyntaxText>,
}
impl SyntaxFileBlame {
fn path(&self) -> &str {
&self.file_blame.path
}
fn commit_id(&self) -> &CommitId {
&self.file_blame.commit_id
}
fn lines(&self) -> &Vec<(Option<BlameHunk>, String)> {
&self.file_blame.lines
}
}
enum BlameProcess {
GettingBlame(AsyncBlame),
SyntaxHighlighting {
unstyled_file_blame: SyntaxFileBlame,
job: AsyncSingleJob<AsyncSyntaxJob>,
},
Result(SyntaxFileBlame),
}
impl BlameProcess {
fn result(&self) -> Option<&SyntaxFileBlame> {
match self {
Self::GettingBlame(_) => None,
Self::SyntaxHighlighting {
unstyled_file_blame,
..
} => Some(unstyled_file_blame),
Self::Result(ref file_blame) => Some(file_blame),
}
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct BlameFileOpen { pub struct BlameFileOpen {
pub file_path: String, pub file_path: String,
@ -43,14 +88,16 @@ pub struct BlameFileComponent {
title: String, title: String,
theme: SharedTheme, theme: SharedTheme,
queue: Queue, queue: Queue,
async_blame: AsyncBlame,
visible: bool, visible: bool,
open_request: Option<BlameFileOpen>, open_request: Option<BlameFileOpen>,
params: Option<BlameParams>, params: Option<BlameParams>,
file_blame: Option<FileBlame>,
table_state: std::cell::Cell<TableState>, table_state: std::cell::Cell<TableState>,
key_config: SharedKeyConfig, key_config: SharedKeyConfig,
current_height: std::cell::Cell<usize>, current_height: std::cell::Cell<usize>,
blame: BlameProcess,
app_sender: Sender<AsyncAppNotification>,
git_sender: Sender<AsyncGitNotification>,
repo: RepoPathRef,
} }
impl DrawableComponent for BlameFileComponent { impl DrawableComponent for BlameFileComponent {
fn draw<B: Backend>( fn draw<B: Backend>(
@ -80,6 +127,18 @@ impl DrawableComponent for BlameFileComponent {
]; ];
let number_of_rows = rows.len(); let number_of_rows = rows.len();
let syntax_highlight_progress = match self.blame {
BlameProcess::SyntaxHighlighting {
ref job, ..
} => job
.progress()
.map(|p| format!(" ({}%)", p.progress))
.unwrap_or_default(),
BlameProcess::GettingBlame(_)
| BlameProcess::Result(_) => String::new(),
};
let title_with_highlight_progress =
format!("{title}{syntax_highlight_progress}");
let table = Table::new(rows) let table = Table::new(rows)
.widths(&constraints) .widths(&constraints)
@ -89,7 +148,7 @@ impl DrawableComponent for BlameFileComponent {
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.title(Span::styled( .title(Span::styled(
title, title_with_highlight_progress,
self.theme.title(true), self.theme.title(true),
)) ))
.border_style(self.theme.block(true)), .border_style(self.theme.block(true)),
@ -139,6 +198,7 @@ impl Component for BlameFileComponent {
out: &mut Vec<CommandInfo>, out: &mut Vec<CommandInfo>,
force_all: bool, force_all: bool,
) -> CommandBlocking { ) -> CommandBlocking {
let file_blame = self.blame.result();
if self.is_visible() || force_all { if self.is_visible() || force_all {
out.push( out.push(
CommandInfo::new( CommandInfo::new(
@ -152,7 +212,7 @@ impl Component for BlameFileComponent {
CommandInfo::new( CommandInfo::new(
strings::commands::scroll(&self.key_config), strings::commands::scroll(&self.key_config),
true, true,
self.file_blame.is_some(), file_blame.is_some(),
) )
.order(1), .order(1),
); );
@ -162,7 +222,7 @@ impl Component for BlameFileComponent {
&self.key_config, &self.key_config,
), ),
true, true,
self.file_blame.is_some(), file_blame.is_some(),
) )
.order(1), .order(1),
); );
@ -172,7 +232,7 @@ impl Component for BlameFileComponent {
&self.key_config, &self.key_config,
), ),
true, true,
self.file_blame.is_some(), file_blame.is_some(),
) )
.order(1), .order(1),
); );
@ -275,18 +335,20 @@ impl BlameFileComponent {
Self { Self {
title: String::from(title), title: String::from(title),
theme: env.theme.clone(), theme: env.theme.clone(),
async_blame: AsyncBlame::new(
env.repo.borrow().clone(),
&env.sender_git,
),
queue: env.queue.clone(), queue: env.queue.clone(),
visible: false, visible: false,
params: None, params: None,
file_blame: None,
open_request: None, open_request: None,
table_state: std::cell::Cell::new(TableState::default()), table_state: std::cell::Cell::new(TableState::default()),
key_config: env.key_config.clone(), key_config: env.key_config.clone(),
current_height: std::cell::Cell::new(0), current_height: std::cell::Cell::new(0),
app_sender: env.sender_app.clone(),
git_sender: env.sender_git.clone(),
blame: BlameProcess::GettingBlame(AsyncBlame::new(
env.repo.borrow().clone(),
&env.sender_git,
)),
repo: env.repo.clone(),
} }
} }
@ -314,10 +376,12 @@ impl BlameFileComponent {
file_path: open.file_path, file_path: open.file_path,
commit_id: open.commit_id, commit_id: open.commit_id,
}); });
self.file_blame = None; self.blame = BlameProcess::GettingBlame(AsyncBlame::new(
self.repo.borrow().clone(),
&self.git_sender,
));
self.table_state.get_mut().select(Some(0)); self.table_state.get_mut().select(Some(0));
self.visible = true; self.visible = true;
self.update()?; self.update()?;
Ok(()) Ok(())
@ -325,11 +389,22 @@ impl BlameFileComponent {
/// ///
pub fn any_work_pending(&self) -> bool { pub fn any_work_pending(&self) -> bool {
self.async_blame.is_pending() !matches!(self.blame, BlameProcess::Result(_))
} }
/// pub fn update_async(
pub fn update_git( &mut self,
ev: AsyncNotification,
) -> Result<()> {
if let AsyncNotification::Git(ev) = ev {
return self.update_git(ev);
}
self.update_syntax(ev);
Ok(())
}
fn update_git(
&mut self, &mut self,
event: AsyncGitNotification, event: AsyncGitNotification,
) -> Result<()> { ) -> Result<()> {
@ -342,33 +417,87 @@ impl BlameFileComponent {
fn update(&mut self) -> Result<()> { fn update(&mut self) -> Result<()> {
if self.is_visible() { if self.is_visible() {
if let Some(params) = &self.params { match self.blame {
if let Some(( BlameProcess::Result(_)
previous_blame_params, | BlameProcess::SyntaxHighlighting { .. } => {}
last_file_blame, BlameProcess::GettingBlame(ref mut async_blame) => {
)) = self.async_blame.last()? if let Some(params) = &self.params {
{ if let Some((
if previous_blame_params == *params { previous_blame_params,
self.file_blame = Some(last_file_blame); last_file_blame,
self.set_open_selection(); )) = async_blame.last()?
{
if previous_blame_params == *params {
self.blame = BlameProcess::SyntaxHighlighting {
unstyled_file_blame: SyntaxFileBlame {
file_blame: last_file_blame,
styled_text: None,
},
job: AsyncSingleJob::new(
self.app_sender.clone(),
)
};
self.set_open_selection();
self.highlight_blame_lines();
return Ok(()); return Ok(());
}
}
async_blame.request(params.clone())?;
} }
} }
self.async_blame.request(params.clone())?;
} }
} }
Ok(()) Ok(())
} }
fn update_syntax(&mut self, ev: AsyncNotification) {
let BlameProcess::SyntaxHighlighting {
ref unstyled_file_blame,
ref job,
} = self.blame
else {
return;
};
if let AsyncNotification::App(
AsyncAppNotification::SyntaxHighlighting(progress),
) = ev
{
match progress {
SyntaxHighlightProgress::Done => {
if let Some(job) = job.take_last() {
if let Some(syntax) = job.result() {
if syntax.path()
== Path::new(
unstyled_file_blame.path(),
) {
self.blame = BlameProcess::Result(
SyntaxFileBlame {
file_blame:
unstyled_file_blame
.file_blame
.clone(),
styled_text: Some(syntax),
},
);
}
}
}
}
SyntaxHighlightProgress::Progress => {}
}
}
}
/// ///
fn get_title(&self) -> String { fn get_title(&self) -> String {
match ( match (
self.any_work_pending(), self.any_work_pending(),
self.params.as_ref(), self.params.as_ref(),
self.file_blame.as_ref(), self.blame.result(),
) { ) {
(true, Some(params), _) => { (true, Some(params), _) => {
format!( format!(
@ -381,7 +510,7 @@ impl BlameFileComponent {
"{} -- {} -- {}", "{} -- {} -- {}",
self.title, self.title,
params.file_path, params.file_path,
file_blame.commit_id.get_short_string() file_blame.commit_id().get_short_string()
) )
} }
(false, Some(params), None) => { (false, Some(params), None) => {
@ -396,11 +525,16 @@ impl BlameFileComponent {
/// ///
fn get_rows(&self, width: usize) -> Vec<Row> { fn get_rows(&self, width: usize) -> Vec<Row> {
self.file_blame let file_blame = self.blame.result();
.as_ref()
.map_or_else(Vec::new, |file_blame| { file_blame
.map(|file_blame| {
let styled_text: Option<Text<'_>> = file_blame
.styled_text
.as_ref()
.map(std::convert::Into::into);
file_blame file_blame
.lines .lines()
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, (blame_hunk, line))| { .map(|(i, (blame_hunk, line))| {
@ -409,26 +543,55 @@ impl BlameFileComponent {
i, i,
(blame_hunk.as_ref(), line.as_ref()), (blame_hunk.as_ref(), line.as_ref()),
file_blame, file_blame,
&styled_text,
) )
}) })
.collect() .collect()
}) })
.unwrap_or_default()
} }
fn get_line_blame( fn highlight_blame_lines(&mut self) {
&self, let BlameProcess::SyntaxHighlighting {
ref unstyled_file_blame,
ref mut job,
} = self.blame
else {
return;
};
let Some(params) = &self.params else {
return;
};
let raw_lines = unstyled_file_blame
.lines()
.iter()
.map(|l| l.1.clone())
.collect::<Vec<_>>();
let text = tabs_to_spaces(raw_lines.join("\n"));
job.spawn(AsyncSyntaxJob::new(
text,
params.file_path.clone(),
));
}
fn get_line_blame<'a>(
&'a self,
width: usize, width: usize,
line_number: usize, line_number: usize,
hunk_and_line: (Option<&BlameHunk>, &str), hunk_and_line: (Option<&BlameHunk>, &str),
file_blame: &FileBlame, file_blame: &'a SyntaxFileBlame,
) -> Row { styled_text: &Option<Text<'a>>,
) -> Row<'a> {
let (hunk_for_line, line) = hunk_and_line; let (hunk_for_line, line) = hunk_and_line;
let show_metadata = if line_number == 0 { let show_metadata = if line_number == 0 {
true true
} else { } else {
let hunk_for_previous_line = let hunk_for_previous_line =
&file_blame.lines[line_number - 1]; &file_blame.lines()[line_number - 1];
match (hunk_for_previous_line, hunk_for_line) { match (hunk_for_previous_line, hunk_for_line) {
((Some(previous), _), Some(current)) => { ((Some(previous), _), Some(current)) => {
@ -445,16 +608,26 @@ impl BlameFileComponent {
}; };
let line_number_width = self.get_line_number_width(); let line_number_width = self.get_line_number_width();
let text_cell = styled_text.as_ref().map_or_else(
|| {
Cell::from(tabs_to_spaces(String::from(line)))
.style(self.theme.text(true, false))
},
|styled_text| {
let styled_text =
styled_text.lines[line_number].clone();
Cell::from(styled_text)
},
);
cells.push( cells.push(
Cell::from(format!( Cell::from(format!(
"{line_number:>line_number_width$}{VERTICAL}", "{line_number:>line_number_width$}{VERTICAL}",
)) ))
.style(self.theme.text(true, false)), .style(self.theme.text(true, false)),
); );
cells.push( cells.push(text_cell);
Cell::from(tabs_to_spaces(String::from(line)))
.style(self.theme.text(true, false)),
);
Row::new(cells) Row::new(cells)
} }
@ -478,12 +651,11 @@ impl BlameFileComponent {
utils::time_to_string(hunk.time, true) utils::time_to_string(hunk.time, true)
}); });
let is_blamed_commit = self let file_blame = self.blame.result();
.file_blame let is_blamed_commit = file_blame
.as_ref()
.and_then(|file_blame| { .and_then(|file_blame| {
blame_hunk.map(|hunk| { blame_hunk.map(|hunk| {
file_blame.commit_id == hunk.commit_id file_blame.commit_id() == &hunk.commit_id
}) })
}) })
.unwrap_or(false); .unwrap_or(false);
@ -498,9 +670,9 @@ impl BlameFileComponent {
} }
fn get_max_line_number(&self) -> usize { fn get_max_line_number(&self) -> usize {
self.file_blame self.blame
.as_ref() .result()
.map_or(0, |file_blame| file_blame.lines.len() - 1) .map_or(0, |file_blame| file_blame.lines().len() - 1)
} }
fn get_line_number_width(&self) -> usize { fn get_line_number_width(&self) -> usize {
@ -551,7 +723,7 @@ impl BlameFileComponent {
} }
fn get_selection(&self) -> Option<usize> { fn get_selection(&self) -> Option<usize> {
self.file_blame.as_ref().and_then(|_| { self.blame.result().as_ref().and_then(|_| {
let table_state = self.table_state.take(); let table_state = self.table_state.take();
let selection = table_state.selected(); let selection = table_state.selected();
@ -563,12 +735,12 @@ impl BlameFileComponent {
} }
fn selected_commit(&self) -> Option<CommitId> { fn selected_commit(&self) -> Option<CommitId> {
self.file_blame.as_ref().and_then(|file_blame| { self.blame.result().as_ref().and_then(|file_blame| {
let table_state = self.table_state.take(); let table_state = self.table_state.take();
let commit_id = let commit_id =
table_state.selected().and_then(|selected| { table_state.selected().and_then(|selected| {
file_blame.lines[selected] file_blame.lines()[selected]
.0 .0
.as_ref() .as_ref()
.map(|hunk| hunk.commit_id) .map(|hunk| hunk.commit_id)