diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e43b09d546..30c389907df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). ## Unreleased +### Changes +- add syntax highlighting for blame view [[@tdtrung17693](https://github.com/tdtrung17693)] ([#745](https://github.com/extrawurst/gitui/issues/745)) ### Added * `theme.ron` now supports customizing line break symbol ([#1894](https://github.com/extrawurst/gitui/issues/1894)) diff --git a/src/app.rs b/src/app.rs index 6fdfc424601..a4f76ae6568 100644 --- a/src/app.rs +++ b/src/app.rs @@ -399,7 +399,6 @@ impl App { self.status_tab.update_git(ev)?; self.stashing_tab.update_git(ev)?; self.revlog.update_git(ev)?; - self.blame_file_popup.update_git(ev)?; self.file_revlog_popup.update_git(ev)?; self.inspect_commit_popup.update_git(ev)?; self.compare_commits_popup.update_git(ev)?; @@ -411,6 +410,7 @@ impl App { } self.files_tab.update_async(ev)?; + self.blame_file_popup.update_async(ev)?; self.revision_files_popup.update(ev)?; self.tags_popup.update(ev); diff --git a/src/components/blame_file.rs b/src/components/blame_file.rs index 2a86aba5cde..6692fc76c70 100644 --- a/src/components/blame_file.rs +++ b/src/components/blame_file.rs @@ -10,29 +10,73 @@ use crate::{ queue::{InternalEvent, Queue, StackablePopupOpen}, string_utils::tabs_to_spaces, strings, - ui::{self, style::SharedTheme}, + ui::{self, style::SharedTheme, AsyncSyntaxJob, SyntaxText}, + AsyncAppNotification, AsyncNotification, SyntaxHighlightProgress, }; use anyhow::Result; use asyncgit::{ + asyncjob::AsyncSingleJob, sync::{BlameHunk, CommitId, FileBlame}, AsyncBlame, AsyncGitNotification, BlameParams, }; +use crossbeam_channel::Sender; use crossterm::event::Event; use ratatui::{ backend::Backend, layout::{Constraint, Rect}, symbols::line::VERTICAL, - text::Span, + text::{Span, Text}, widgets::{Block, Borders, Cell, Clear, Row, Table, TableState}, Frame, }; -use std::convert::TryInto; +use std::{convert::TryInto, path::Path}; static NO_COMMIT_ID: &str = "0000000"; static NO_AUTHOR: &str = ""; static MIN_AUTHOR_WIDTH: usize = 3; static MAX_AUTHOR_WIDTH: usize = 20; +struct SyntaxFileBlame { + pub file_blame: FileBlame, + pub styled_text: Option, +} + +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, String)> { + &self.file_blame.lines + } +} + +enum BlameProcess { + GettingBlame(AsyncBlame), + SyntaxHighlighting { + unstyled_file_blame: SyntaxFileBlame, + job: AsyncSingleJob, + }, + 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)] pub struct BlameFileOpen { pub file_path: String, @@ -44,14 +88,14 @@ pub struct BlameFileComponent { title: String, theme: SharedTheme, queue: Queue, - async_blame: AsyncBlame, visible: bool, open_request: Option, params: Option, - file_blame: Option, table_state: std::cell::Cell, key_config: SharedKeyConfig, current_height: std::cell::Cell, + blame: BlameProcess, + app_sender: Sender, } impl DrawableComponent for BlameFileComponent { fn draw( @@ -81,6 +125,18 @@ impl DrawableComponent for BlameFileComponent { ]; 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) .widths(&constraints) @@ -90,7 +146,7 @@ impl DrawableComponent for BlameFileComponent { Block::default() .borders(Borders::ALL) .title(Span::styled( - title, + title_with_highlight_progress, self.theme.title(true), )) .border_style(self.theme.block(true)), @@ -140,6 +196,7 @@ impl Component for BlameFileComponent { out: &mut Vec, force_all: bool, ) -> CommandBlocking { + let file_blame = self.blame.result(); if self.is_visible() || force_all { out.push( CommandInfo::new( @@ -153,7 +210,7 @@ impl Component for BlameFileComponent { CommandInfo::new( strings::commands::scroll(&self.key_config), true, - self.file_blame.is_some(), + file_blame.is_some(), ) .order(1), ); @@ -163,7 +220,7 @@ impl Component for BlameFileComponent { &self.key_config, ), true, - self.file_blame.is_some(), + file_blame.is_some(), ) .order(1), ); @@ -173,7 +230,7 @@ impl Component for BlameFileComponent { &self.key_config, ), true, - self.file_blame.is_some(), + file_blame.is_some(), ) .order(1), ); @@ -276,18 +333,18 @@ impl BlameFileComponent { Self { title: String::from(title), theme: env.theme.clone(), - async_blame: AsyncBlame::new( - env.repo.borrow().clone(), - &env.sender_git, - ), queue: env.queue.clone(), visible: false, params: None, - file_blame: None, open_request: None, table_state: std::cell::Cell::new(TableState::default()), key_config: env.key_config.clone(), current_height: std::cell::Cell::new(0), + app_sender: env.sender_app.clone(), + blame: BlameProcess::GettingBlame(AsyncBlame::new( + env.repo.borrow().clone(), + &env.sender_git, + )), } } @@ -315,10 +372,8 @@ impl BlameFileComponent { file_path: open.file_path, commit_id: open.commit_id, }); - self.file_blame = None; self.table_state.get_mut().select(Some(0)); self.visible = true; - self.update()?; Ok(()) @@ -326,11 +381,22 @@ impl BlameFileComponent { /// pub fn any_work_pending(&self) -> bool { - self.async_blame.is_pending() + !matches!(self.blame, BlameProcess::Result(_)) } - /// - pub fn update_git( + pub fn update_async( + &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, event: AsyncGitNotification, ) -> Result<()> { @@ -343,33 +409,87 @@ impl BlameFileComponent { fn update(&mut self) -> Result<()> { if self.is_visible() { - if let Some(params) = &self.params { - if let Some(( - previous_blame_params, - last_file_blame, - )) = self.async_blame.last()? - { - if previous_blame_params == *params { - self.file_blame = Some(last_file_blame); - self.set_open_selection(); - - return Ok(()); + match self.blame { + BlameProcess::Result(_) + | BlameProcess::SyntaxHighlighting { .. } => {} + BlameProcess::GettingBlame(ref mut async_blame) => { + if let Some(params) = &self.params { + if let Some(( + previous_blame_params, + last_file_blame, + )) = 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(()); + } + } + + async_blame.request(params.clone())?; } } - - self.async_blame.request(params.clone())?; } } 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 { match ( self.any_work_pending(), self.params.as_ref(), - self.file_blame.as_ref(), + self.blame.result(), ) { (true, Some(params), _) => { format!( @@ -382,7 +502,7 @@ impl BlameFileComponent { "{} -- {} -- {}", self.title, params.file_path, - file_blame.commit_id.get_short_string() + file_blame.commit_id().get_short_string() ) } (false, Some(params), None) => { @@ -397,11 +517,16 @@ impl BlameFileComponent { /// fn get_rows(&self, width: usize) -> Vec { - self.file_blame - .as_ref() - .map_or_else(Vec::new, |file_blame| { + let file_blame = self.blame.result(); + + file_blame + .map(|file_blame| { + let styled_text: Option> = file_blame + .styled_text + .as_ref() + .map(std::convert::Into::into); file_blame - .lines + .lines() .iter() .enumerate() .map(|(i, (blame_hunk, line))| { @@ -410,26 +535,55 @@ impl BlameFileComponent { i, (blame_hunk.as_ref(), line.as_ref()), file_blame, + &styled_text, ) }) .collect() }) + .unwrap_or_default() } - fn get_line_blame( - &self, + fn highlight_blame_lines(&mut 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::>(); + 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, line_number: usize, hunk_and_line: (Option<&BlameHunk>, &str), - file_blame: &FileBlame, - ) -> Row { + file_blame: &'a SyntaxFileBlame, + styled_text: &Option>, + ) -> Row<'a> { let (hunk_for_line, line) = hunk_and_line; let show_metadata = if line_number == 0 { true } else { 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) { ((Some(previous), _), Some(current)) => { @@ -446,16 +600,26 @@ impl BlameFileComponent { }; 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( Cell::from(format!( "{line_number:>line_number_width$}{VERTICAL}", )) .style(self.theme.text(true, false)), ); - cells.push( - Cell::from(tabs_to_spaces(String::from(line))) - .style(self.theme.text(true, false)), - ); + cells.push(text_cell); Row::new(cells) } @@ -479,12 +643,11 @@ impl BlameFileComponent { utils::time_to_string(hunk.time, true) }); - let is_blamed_commit = self - .file_blame - .as_ref() + let file_blame = self.blame.result(); + let is_blamed_commit = file_blame .and_then(|file_blame| { blame_hunk.map(|hunk| { - file_blame.commit_id == hunk.commit_id + file_blame.commit_id() == &hunk.commit_id }) }) .unwrap_or(false); @@ -499,9 +662,9 @@ impl BlameFileComponent { } fn get_max_line_number(&self) -> usize { - self.file_blame - .as_ref() - .map_or(0, |file_blame| file_blame.lines.len() - 1) + self.blame + .result() + .map_or(0, |file_blame| file_blame.lines().len() - 1) } fn get_line_number_width(&self) -> usize { @@ -552,7 +715,7 @@ impl BlameFileComponent { } fn get_selection(&self) -> Option { - self.file_blame.as_ref().and_then(|_| { + self.blame.result().as_ref().and_then(|_| { let table_state = self.table_state.take(); let selection = table_state.selected(); @@ -564,12 +727,12 @@ impl BlameFileComponent { } fn selected_commit(&self) -> Option { - 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 commit_id = table_state.selected().and_then(|selected| { - file_blame.lines[selected] + file_blame.lines()[selected] .0 .as_ref() .map(|hunk| hunk.commit_id)