From 0454e2a1cdc4c8db90944fc7dbf1069c6d32555d Mon Sep 17 00:00:00 2001 From: Stephan Dilly Date: Thu, 2 Sep 2021 13:14:36 +0200 Subject: [PATCH] asyncjob supports sending arbitrary notifications this is used to send progress reports during work on the job --- asyncgit/src/asyncjob/mod.rs | 14 ++-- asyncgit/src/error.rs | 16 +++++ asyncgit/src/lib.rs | 2 + asyncgit/src/progress.rs | 2 +- asyncgit/src/remote_tags.rs | 9 ++- src/components/syntax_text.rs | 60 +++++++++++------ src/main.rs | 8 ++- src/ui/syntax_text.rs | 121 +++++++++++++++++++++++++++------- 8 files changed, 179 insertions(+), 53 deletions(-) diff --git a/asyncgit/src/asyncjob/mod.rs b/asyncgit/src/asyncjob/mod.rs index 8317ba65..3b6aa623 100644 --- a/asyncgit/src/asyncjob/mod.rs +++ b/asyncgit/src/asyncjob/mod.rs @@ -12,7 +12,10 @@ pub trait AsyncJob: Send + Sync + Clone { type Notification: Copy + Send + 'static; /// can run a synchronous time intensive task - fn run(&mut self) -> Self::Notification; + fn run( + &mut self, + sender: Sender, + ) -> Result; } /// Abstraction for a FIFO task queue that will only queue up **one** `next` job. @@ -95,7 +98,7 @@ impl AsyncSingleJob { { let _pending = self.pending.lock()?; - let notification = task.run(); + let notification = task.run(self.sender.clone())?; if let Ok(mut last) = self.last.lock() { *last = Some(task); @@ -147,7 +150,10 @@ mod test { impl AsyncJob for TestJob { type Notification = TestNotificaton; - fn run(&mut self) -> Self::Notification { + fn run( + &mut self, + _sender: Sender, + ) -> Result { println!("[job] wait"); while !self.finish.load(Ordering::SeqCst) { @@ -165,7 +171,7 @@ mod test { println!("[job] value: {}", res); - () + Ok(()) } } diff --git a/asyncgit/src/error.rs b/asyncgit/src/error.rs index fe7bc214..9f2a2566 100644 --- a/asyncgit/src/error.rs +++ b/asyncgit/src/error.rs @@ -3,51 +3,67 @@ use std::{num::TryFromIntError, string::FromUtf8Error}; use thiserror::Error; +/// #[derive(Error, Debug)] pub enum Error { + /// #[error("`{0}`")] Generic(String), + /// #[error("git: no head found")] NoHead, + /// #[error("git: conflict during rebase")] RebaseConflict, + /// #[error("git: remote url not found")] UnknownRemote, + /// #[error("git: inconclusive remotes")] NoDefaultRemoteFound, + /// #[error("git: work dir error")] NoWorkDir, + /// #[error("git: uncommitted changes")] UncommittedChanges, + /// #[error("git: can\u{2019}t run blame on a binary file")] NoBlameOnBinaryFile, + /// #[error("binary file")] BinaryFile, + /// #[error("io error:{0}")] Io(#[from] std::io::Error), + /// #[error("git error:{0}")] Git(#[from] git2::Error), + /// #[error("utf8 error:{0}")] Utf8Conversion(#[from] FromUtf8Error), + /// #[error("TryFromInt error:{0}")] IntConversion(#[from] TryFromIntError), + /// #[error("EasyCast error:{0}")] EasyCast(#[from] easy_cast::Error), } +/// pub type Result = std::result::Result; impl From> for Error { diff --git a/asyncgit/src/lib.rs b/asyncgit/src/lib.rs index 586470f3..9530817c 100644 --- a/asyncgit/src/lib.rs +++ b/asyncgit/src/lib.rs @@ -43,7 +43,9 @@ pub use crate::{ blame::{AsyncBlame, BlameParams}, commit_files::{AsyncCommitFiles, CommitFilesParams}, diff::{AsyncDiff, DiffParams, DiffType}, + error::{Error, Result}, fetch::{AsyncFetch, FetchRequest}, + progress::ProgressPercent, push::{AsyncPush, PushRequest}, push_tags::{AsyncPushTags, PushTagsRequest}, remote_progress::{RemoteProgress, RemoteProgressState}, diff --git a/asyncgit/src/progress.rs b/asyncgit/src/progress.rs index 3c58918c..31c41e22 100644 --- a/asyncgit/src/progress.rs +++ b/asyncgit/src/progress.rs @@ -4,7 +4,7 @@ use easy_cast::{Conv, ConvFloat}; use std::cmp; /// -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct ProgressPercent { /// percent 0..100 pub progress: u8, diff --git a/asyncgit/src/remote_tags.rs b/asyncgit/src/remote_tags.rs index b3da2964..d4de3c05 100644 --- a/asyncgit/src/remote_tags.rs +++ b/asyncgit/src/remote_tags.rs @@ -1,5 +1,7 @@ //! +use crossbeam_channel::Sender; + use crate::{ asyncjob::AsyncJob, error::Result, @@ -52,7 +54,10 @@ impl AsyncRemoteTagsJob { impl AsyncJob for AsyncRemoteTagsJob { type Notification = AsyncGitNotification; - fn run(&mut self) -> Self::Notification { + fn run( + &mut self, + _sender: Sender, + ) -> Result { if let Ok(mut state) = self.state.lock() { *state = state.take().map(|state| match state { JobState::Request(basic_credential) => { @@ -73,6 +78,6 @@ impl AsyncJob for AsyncRemoteTagsJob { }); } - AsyncGitNotification::RemoteTags + Ok(AsyncGitNotification::RemoteTags) } } diff --git a/src/components/syntax_text.rs b/src/components/syntax_text.rs index f183ea20..728d0e05 100644 --- a/src/components/syntax_text.rs +++ b/src/components/syntax_text.rs @@ -9,13 +9,13 @@ use crate::{ self, common_nav, style::SharedTheme, AsyncSyntaxJob, ParagraphState, ScrollPos, StatefulParagraph, }, - AsyncAppNotification, AsyncNotification, + AsyncAppNotification, AsyncNotification, SyntaxHighlightProgress, }; use anyhow::Result; use asyncgit::{ asyncjob::AsyncSingleJob, sync::{self, TreeFile}, - CWD, + ProgressPercent, CWD, }; use crossbeam_channel::Sender; use crossterm::event::Event; @@ -33,6 +33,7 @@ use tui::{ pub struct SyntaxTextComponent { current_file: Option<(String, Either)>, async_highlighting: AsyncSingleJob, + syntax_progress: Option, key_config: SharedKeyConfig, paragraph_state: Cell, focused: bool, @@ -48,6 +49,7 @@ impl SyntaxTextComponent { ) -> Self { Self { async_highlighting: AsyncSingleJob::new(sender.clone()), + syntax_progress: None, current_file: None, paragraph_state: Cell::new(ParagraphState::default()), focused: false, @@ -58,19 +60,27 @@ impl SyntaxTextComponent { /// pub fn update(&mut self, ev: AsyncNotification) { - if matches!( - ev, - AsyncNotification::App( - AsyncAppNotification::SyntaxHighlighting - ) - ) { - if let Some(job) = self.async_highlighting.take_last() { - if let Some((path, content)) = - self.current_file.as_mut() - { - if let Some(syntax) = job.result() { - if syntax.path() == Path::new(path) { - *content = Either::Left(syntax); + if let AsyncNotification::App( + AsyncAppNotification::SyntaxHighlighting(progress), + ) = ev + { + match progress { + SyntaxHighlightProgress::Progress(progress) => { + self.syntax_progress = Some(progress); + } + SyntaxHighlightProgress::Done => { + self.syntax_progress = None; + if let Some(job) = + self.async_highlighting.take_last() + { + if let Some((path, content)) = + self.current_file.as_mut() + { + if let Some(syntax) = job.result() { + if syntax.path() == Path::new(path) { + *content = Either::Left(syntax); + } + } } } } @@ -101,6 +111,8 @@ impl SyntaxTextComponent { match sync::tree_file_content(CWD, item) { Ok(content) => { let content = tabs_to_spaces(content); + self.syntax_progress = + Some(ProgressPercent::empty()); self.async_highlighting.spawn( AsyncSyntaxJob::new( content.clone(), @@ -185,16 +197,22 @@ impl DrawableComponent for SyntaxTextComponent { }, ); + let title = format!( + "{}{}", + self.current_file + .as_ref() + .map(|(name, _)| name.clone()) + .unwrap_or_default(), + self.syntax_progress + .map(|p| format!(" ({}%)", p.progress)) + .unwrap_or_default() + ); + let content = StatefulParagraph::new(text) .wrap(Wrap { trim: false }) .block( Block::default() - .title( - self.current_file - .as_ref() - .map(|(name, _)| name.clone()) - .unwrap_or_default(), - ) + .title(title) .borders(Borders::ALL) .border_style(self.theme.title(self.focused())), ); diff --git a/src/main.rs b/src/main.rs index 8e59dd44..83974175 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,10 +76,16 @@ pub enum QueueEvent { InputEvent(InputEvent), } +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum SyntaxHighlightProgress { + Progress(asyncgit::ProgressPercent), + Done, +} + #[derive(Clone, Copy, Debug, PartialEq)] pub enum AsyncAppNotification { /// - SyntaxHighlighting, + SyntaxHighlighting(SyntaxHighlightProgress), } #[derive(Clone, Copy, Debug, PartialEq)] diff --git a/src/ui/syntax_text.rs b/src/ui/syntax_text.rs index 8e0f71eb..c1b2f36c 100644 --- a/src/ui/syntax_text.rs +++ b/src/ui/syntax_text.rs @@ -1,4 +1,5 @@ -use asyncgit::asyncjob::AsyncJob; +use asyncgit::{asyncjob::AsyncJob, ProgressPercent}; +use crossbeam_channel::Sender; use lazy_static::lazy_static; use scopetime::scope_time; use std::{ @@ -6,6 +7,7 @@ use std::{ ops::Range, path::{Path, PathBuf}, sync::{Arc, Mutex}, + time::{Duration, Instant}, }; use syntect::{ highlighting::{ @@ -16,7 +18,7 @@ use syntect::{ }; use tui::text::{Span, Spans}; -use crate::AsyncAppNotification; +use crate::{AsyncAppNotification, SyntaxHighlightProgress}; struct SyntaxLine { items: Vec<(Style, usize, Range)>, @@ -34,12 +36,47 @@ lazy_static! { static ref THEME_SET: ThemeSet = ThemeSet::load_defaults(); } +pub struct AsyncProgressBuffer { + current: usize, + total: usize, + last_send: Option, + min_interval: Duration, +} + +impl AsyncProgressBuffer { + pub const fn new(total: usize, min_interval: Duration) -> Self { + Self { + current: 0, + total, + last_send: None, + min_interval, + } + } + + pub fn send_progress(&mut self) -> ProgressPercent { + self.last_send = Some(Instant::now()); + ProgressPercent::new(self.current, self.total) + } + + pub fn update(&mut self, current: usize) -> bool { + self.current = current; + self.last_send.map_or(true, |last_send| { + last_send.elapsed() > self.min_interval + }) + } +} + impl SyntaxText { - pub fn new(text: String, file_path: &Path) -> Self { + pub fn new( + text: String, + file_path: &Path, + sender: &Sender, + ) -> asyncgit::Result { scope_time!("syntax_highlighting"); log::debug!("syntax: {:?}", file_path); let mut state = { + scope_time!("syntax_highlighting.0"); let syntax = file_path .extension() .and_then(OsStr::to_str) @@ -66,27 +103,53 @@ impl SyntaxText { let mut highlight_state = HighlightState::new(&highlighter, ScopeStack::new()); - for (number, line) in text.lines().enumerate() { - let ops = state.parse_line(line, &SYNTAX_SET); - let iter = RangedHighlightIterator::new( - &mut highlight_state, - &ops[..], - line, - &highlighter, - ); + { + let total_count = text.lines().count(); - syntax_lines.push(SyntaxLine { - items: iter - .map(|(style, _, range)| (style, number, range)) - .collect(), - }); + let mut buffer = AsyncProgressBuffer::new( + total_count, + Duration::from_millis(200), + ); + sender.send(AsyncAppNotification::SyntaxHighlighting( + SyntaxHighlightProgress::Progress( + buffer.send_progress(), + ), + ))?; + + for (number, line) in text.lines().enumerate() { + let ops = state.parse_line(line, &SYNTAX_SET); + let iter = RangedHighlightIterator::new( + &mut highlight_state, + &ops[..], + line, + &highlighter, + ); + + syntax_lines.push(SyntaxLine { + items: iter + .map(|(style, _, range)| { + (style, number, range) + }) + .collect(), + }); + + if buffer.update(number) { + sender.send( + AsyncAppNotification::SyntaxHighlighting( + SyntaxHighlightProgress::Progress( + buffer.send_progress(), + ), + ), + )?; + } + } } - Self { + Ok(Self { text, lines: syntax_lines, path: file_path.into(), - } + }) } /// @@ -179,18 +242,28 @@ impl AsyncSyntaxJob { impl AsyncJob for AsyncSyntaxJob { type Notification = AsyncAppNotification; - fn run(&mut self) -> Self::Notification { - if let Ok(mut state) = self.state.lock() { - *state = state.take().map(|state| match state { + fn run( + &mut self, + sender: Sender, + ) -> asyncgit::Result { + let mut state_mutex = self.state.lock()?; + + if let Some(state) = state_mutex.take() { + *state_mutex = Some(match state { JobState::Request((content, path)) => { - let syntax = - SyntaxText::new(content, Path::new(&path)); + let syntax = SyntaxText::new( + content, + Path::new(&path), + &sender, + )?; JobState::Response(syntax) } JobState::Response(res) => JobState::Response(res), }); } - AsyncAppNotification::SyntaxHighlighting + Ok(AsyncAppNotification::SyntaxHighlighting( + SyntaxHighlightProgress::Done, + )) } }