diff --git a/CHANGELOG.md b/CHANGELOG.md index 68c2158509..13f9d9aa4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * support x509 commit signing [[@kaden-l-nelson](https://github.com/kaden-l-nelson)] ([#2514](https://github.com/gitui-org/gitui/issues/2514)) +* syntax highlighting in the diff view ([Shift+X] cycles style: off / gutter / +- sign / tint) [[@stevenpollack](https://github.com/stevenpollack)] ([#2997](https://github.com/gitui-org/gitui/pull/2997)) ### Changed * use [tombi](https://github.com/tombi-toml/tombi) for all toml file formatting diff --git a/src/components/diff.rs b/src/components/diff.rs index 04779caada..bdf5fdf628 100644 --- a/src/components/diff.rs +++ b/src/components/diff.rs @@ -1,7 +1,9 @@ use super::{ + diff_highlight::{AsyncDiffHighlightJob, LineHighlight}, utils::scroll_horizontal::HorizontalScroll, - utils::scroll_vertical::VerticalScroll, CommandBlocking, - Direction, DrawableComponent, HorizontalScrollType, ScrollType, + utils::scroll_vertical::VerticalScroll, + CommandBlocking, Direction, DrawableComponent, + HorizontalScrollType, ScrollType, }; use crate::{ app::Environment, @@ -16,6 +18,7 @@ use crate::{ }; use anyhow::Result; use asyncgit::{ + asyncjob::AsyncSingleJob, hash, sync::{self, diff::DiffLinePosition, RepoPathRef}, DiffLine, DiffLineType, FileDiff, @@ -24,12 +27,30 @@ use bytesize::ByteSize; use crossterm::event::Event; use ratatui::{ layout::Rect, + style::Style, symbols, text::{Line, Span}, widgets::{Block, Borders, Paragraph}, Frame, }; -use std::{borrow::Cow, cell::Cell, cmp, path::Path}; +use std::{ + borrow::Cow, + cell::{Cell, RefCell}, + cmp, + ops::Range, + path::Path, +}; +use unicode_width::UnicodeWidthStr; + +/// Bundles the two hunk-position flags `get_line_to_add` needs to +/// pick the left gutter glyph/style — kept together to stay under +/// clippy's `too_many_arguments` limit now that the method also +/// takes `&self`. +#[derive(Clone, Copy)] +struct HunkLineFlags { + selected_hunk: bool, + end_of_hunk: bool, +} #[derive(Default)] struct Current { @@ -119,6 +140,8 @@ pub struct DiffComponent { key_config: SharedKeyConfig, is_immutable: bool, options: SharedOptions, + syntax_highlight: RefCell>>, + highlight_job: AsyncSingleJob, } impl DiffComponent { @@ -141,6 +164,10 @@ impl DiffComponent { is_immutable, repo: env.repo.clone(), options: env.options.clone(), + syntax_highlight: RefCell::new(Vec::new()), + highlight_job: AsyncSingleJob::new( + env.sender_app.clone(), + ), } } /// @@ -165,6 +192,8 @@ impl DiffComponent { self.selection = Selection::Single(0); self.selected_hunk = None; self.pending = pending; + self.syntax_highlight.borrow_mut().clear(); + self.highlight_job.cancel(); } /// pub fn update( @@ -218,6 +247,59 @@ impl DiffComponent { }; self.update_selection(old_selection); } + + self.spawn_highlight(); + } + } + + const MAX_HIGHLIGHT_LINES: usize = 10_000; + + /// Spawns the syntect highlight for the current diff on the + /// shared threadpool (see [`AsyncDiffHighlightJob`]). The result + /// is picked up later by [`Self::poll_highlight`] during `draw`. + fn spawn_highlight(&self) { + self.syntax_highlight.borrow_mut().clear(); + + if !self.options.borrow().diff_highlight_style().is_on() { + self.highlight_job.cancel(); + return; + } + + let Some(diff) = self.diff.as_ref() else { + return; + }; + + if diff.hunks.is_empty() + || diff.lines > Self::MAX_HIGHLIGHT_LINES + { + return; + } + + let lines: Vec<(String, DiffLineType)> = diff + .hunks + .iter() + .flat_map(|h| h.lines.iter()) + .map(|l| (l.content.as_ref().to_string(), l.line_type)) + .collect(); + + let path = self.current.path.clone(); + let theme = self.theme.get_syntax(); + let hash = self.current.hash; + + self.highlight_job.spawn(AsyncDiffHighlightJob::new( + hash, lines, path, theme, + )); + } + + /// Applies a finished highlight job's result if it matches the + /// currently displayed diff (stale results are discarded). + fn poll_highlight(&self) { + if let Some(job) = self.highlight_job.take_last() { + if let Some((hash, result)) = job.result() { + if hash == self.current.hash { + *self.syntax_highlight.borrow_mut() = result; + } + } } } @@ -335,6 +417,8 @@ impl DiffComponent { } else { let mut res: Vec = Vec::new(); + let highlights = self.syntax_highlight.borrow(); + let min = self.vertical_scroll.get_top(); let max = min + height as usize; @@ -361,19 +445,32 @@ impl DiffComponent { if line_cursor >= min && line_cursor <= max { - res.push(Self::get_line_to_add( - width, - line, - self.focused() - && self - .selection - .contains(line_cursor), - hunk_selected, - i == hunk_len - 1, - &self.theme, - self.horizontal_scroll - .get_right(), - )); + let highlight = highlights + .get(line_cursor) + .and_then(Option::as_ref) + .map(Vec::as_slice); + + res.push( + self.get_line_to_add( + width, + line, + self.focused() + && self + .selection + .contains( + line_cursor, + ), + HunkLineFlags { + selected_hunk: + hunk_selected, + end_of_hunk: i + == hunk_len - 1, + }, + self.horizontal_scroll + .get_right(), + highlight, + ), + ); lines_added += 1; } @@ -423,21 +520,30 @@ impl DiffComponent { ])] } - fn get_line_to_add<'a>( + fn get_line_to_add( + &self, width: u16, - line: &'a DiffLine, + line: &DiffLine, selected: bool, - selected_hunk: bool, - end_of_hunk: bool, - theme: &SharedTheme, + hunk_flags: HunkLineFlags, scrolled_right: usize, - ) -> Line<'a> { - let style = theme.diff_hunk_marker(selected_hunk); + highlight: Option<&[(Range, Style)]>, + ) -> Line<'_> { + let theme = &self.theme; + let hl_style = self.options.borrow().diff_highlight_style(); + + let style = if hunk_flags.selected_hunk { + theme.diff_hunk_marker(true) + } else if hl_style.color_gutter() { + theme.diff_line(line.line_type, false) + } else { + theme.diff_hunk_marker(false) + }; let is_content_line = matches!(line.line_type, DiffLineType::None); - let left_side_of_line = if end_of_hunk { + let left_side_of_line = if hunk_flags.end_of_hunk { Span::styled(Cow::from(symbols::line::BOTTOM_LEFT), style) } else { match line.line_type { @@ -458,6 +564,24 @@ impl DiffComponent { } else { tabs_to_spaces(line.content.as_ref().to_string()) }; + + if let Some(segments) = highlight { + if !segments.is_empty() + && !matches!(line.line_type, DiffLineType::Header) + { + let mut spans = vec![left_side_of_line.clone()]; + spans.extend(self.highlighted_content_spans( + width, + line.line_type, + selected, + scrolled_right, + segments, + &content, + )); + return Line::from(spans); + } + } + let content = trim_offset(&content, scrolled_right); let filled = if selected { @@ -477,6 +601,86 @@ impl DiffComponent { ]) } + /// Builds the syntect-highlighted content spans (sign glyph, + /// per-token foreground spans, optional row tint/pad) for one + /// diff line, honouring the active [`DiffHighlightStyle`]. The + /// caller prepends the left gutter span. + fn highlighted_content_spans( + &self, + width: u16, + line_type: DiffLineType, + selected: bool, + scrolled_right: usize, + segments: &[(Range, Style)], + content: &str, + ) -> Vec> { + let theme = &self.theme; + let hl_style = self.options.borrow().diff_highlight_style(); + let mut spans: Vec> = Vec::new(); + + if hl_style.shows_sign() { + let (sign, sign_style) = match line_type { + DiffLineType::Add => { + ("+", theme.diff_line(DiffLineType::Add, false)) + } + DiffLineType::Delete => ( + "-", + theme.diff_line(DiffLineType::Delete, false), + ), + _ => (" ", theme.diff_hunk_marker(false)), + }; + spans.push(Span::styled(sign, sign_style)); + } + + let visible = trim_offset(content, scrolled_right); + let start = content.len() - visible.len(); + let bg = if hl_style.shows_tint() { + theme.diff_line_tint(line_type, selected) + } else { + theme.diff_line_highlight_bg(selected) + }; + let mut last = start; + for (range, seg_style) in segments { + let seg_start = range.start.max(start); + let seg_end = range.end.min(content.len()); + if seg_end <= seg_start { + continue; + } + if seg_start > last { + if let Some(t) = content.get(last..seg_start) { + spans.push(Span::styled(t.to_string(), bg)); + } + } + if let Some(t) = content.get(seg_start..seg_end) { + spans.push(Span::styled( + t.to_string(), + seg_style.patch(bg), + )); + } + last = seg_end; + } + if last < content.len() { + if let Some(t) = content.get(last..) { + spans.push(Span::styled(t.to_string(), bg)); + } + } + + let tint_row = hl_style.shows_tint() + && matches!( + line_type, + DiffLineType::Add | DiffLineType::Delete + ); + if selected || tint_row { + let used = UnicodeWidthStr::width(visible); + let pad = (width as usize).saturating_sub(used); + spans.push(Span::styled(format!("{:pad$}\n", ""), bg)); + } else { + spans.push(Span::raw("\n")); + } + + spans + } + const fn hunk_visible( hunk_min: usize, hunk_max: usize, @@ -684,6 +888,8 @@ impl DiffComponent { impl DrawableComponent for DiffComponent { fn draw(&self, f: &mut Frame, r: Rect) -> Result<()> { + self.poll_highlight(); + self.current_size.set(( r.width.saturating_sub(2), r.height.saturating_sub(2), @@ -824,6 +1030,12 @@ impl Component for DiffComponent { self.focused(), )); + out.push(CommandInfo::new( + strings::commands::diff_toggle_syntax(&self.key_config), + true, + self.focused(), + )); + CommandBlocking::PassingOn } @@ -943,6 +1155,24 @@ impl Component for DiffComponent { } else if key_match(e, self.key_config.keys.copy) { self.copy_selection(); Ok(EventState::Consumed) + } else if key_match( + e, + self.key_config.keys.diff_toggle_syntax, + ) { + self.options.borrow_mut().diff_cycle_highlight(); + let style = + self.options.borrow().diff_highlight_style(); + if !style.is_on() { + self.syntax_highlight.borrow_mut().clear(); + self.highlight_job.cancel(); + } else if self + .syntax_highlight + .borrow() + .is_empty() + { + self.spawn_highlight(); + } + Ok(EventState::Consumed) } else { Ok(EventState::NotConsumed) }; @@ -981,16 +1211,21 @@ mod tests { { let default_theme = Rc::new(Theme::default()); + let mut env = Environment::test_env(); + env.theme = default_theme.clone(); + let diff = DiffComponent::new(&env, false); assert_eq!( - DiffComponent::get_line_to_add( + diff.get_line_to_add( 4, &diff_line, false, - false, - false, - &default_theme, - 0 + HunkLineFlags { + selected_hunk: false, + end_of_hunk: false, + }, + 0, + None, ) .spans .last() @@ -1018,10 +1253,21 @@ mod tests { let theme = Rc::new(Theme::init(&file.path().to_path_buf())); + let mut env = Environment::test_env(); + env.theme = theme.clone(); + let diff = DiffComponent::new(&env, false); assert_eq!( - DiffComponent::get_line_to_add( - 4, &diff_line, false, false, false, &theme, 0 + diff.get_line_to_add( + 4, + &diff_line, + false, + HunkLineFlags { + selected_hunk: false, + end_of_hunk: false, + }, + 0, + None, ) .spans .last() diff --git a/src/components/diff_highlight.rs b/src/components/diff_highlight.rs new file mode 100644 index 0000000000..d8b487c4fc --- /dev/null +++ b/src/components/diff_highlight.rs @@ -0,0 +1,255 @@ +//! Off-thread syntax highlighting for the unified diff view. +//! +//! The heavy syntect pass is wrapped in an [`AsyncDiffHighlightJob`] +//! so it can run on the shared threadpool instead of blocking the UI +//! thread during file navigation. + +use crate::ui::SyntaxText; +use crate::{AsyncAppNotification, SyntaxHighlightProgress}; +use asyncgit::{ + asyncjob::{AsyncJob, RunParams}, + DiffLineType, ProgressPercent, +}; +use ratatui::{style::Style, text::Text}; +use std::{ + ops::Range, + path::Path, + sync::{Arc, Mutex}, +}; + +/// Per-line highlight: byte range within the (tab-expanded) line +/// content paired with the syntect-derived style for that token. +pub type LineHighlight = Vec<(Range, Style)>; + +/// Which reconstructed side a diff line was highlighted from. +#[derive(Clone, Copy)] +enum Side { + New(usize), + Old(usize), + Skip, +} + +fn push_expanded_line(buf: &mut String, content: &str) { + let expanded = + crate::string_utils::tabs_to_spaces(content.to_string()); + buf.push_str(expanded.trim_end_matches(['\n', '\r'])); + buf.push('\n'); +} + +/// Highlights one reconstructed side (new or old) of the diff, +/// returning per-line token ranges + styles. +fn highlight_side( + text: &str, + path: &str, + syntax_theme: &str, +) -> Vec { + if text.is_empty() { + return Vec::new(); + } + let Ok(styled) = SyntaxText::new_sync( + text.to_string(), + Path::new(path), + syntax_theme, + ) else { + return Vec::new(); + }; + let rendered: Text = (&styled).into(); + rendered + .lines + .into_iter() + .map(|line| { + let mut offset = 0_usize; + line.spans + .into_iter() + .map(|span| { + let len = span.content.len(); + let range = offset..offset + len; + offset += len; + (range, span.style) + }) + .collect() + }) + .collect() +} + +/// Highlights a diff given its flat, in-order per-line +/// `(content, line_type)` list (same order the diff renders in). +/// +/// Reconstructs the new-side text (`None`/`Add` lines) and old-side +/// text (`None`/`Delete` lines), highlights each independently, then +/// maps every flat line back to its highlight (or `None` for headers +/// and lines that could not be highlighted). +pub fn build_highlight( + lines: &[(String, DiffLineType)], + path: &str, + theme: &str, +) -> Vec> { + let mut new_text = String::new(); + let mut old_text = String::new(); + let mut sides: Vec = Vec::with_capacity(lines.len()); + let (mut ni, mut oi) = (0_usize, 0_usize); + + for (content, line_type) in lines { + match line_type { + DiffLineType::Header => sides.push(Side::Skip), + DiffLineType::Add => { + push_expanded_line(&mut new_text, content); + sides.push(Side::New(ni)); + ni += 1; + } + DiffLineType::Delete => { + push_expanded_line(&mut old_text, content); + sides.push(Side::Old(oi)); + oi += 1; + } + DiffLineType::None => { + push_expanded_line(&mut new_text, content); + push_expanded_line(&mut old_text, content); + sides.push(Side::New(ni)); + ni += 1; + oi += 1; + } + } + } + + let new_hl = highlight_side(&new_text, path, theme); + let old_hl = highlight_side(&old_text, path, theme); + + sides + .into_iter() + .map(|side| match side { + Side::New(i) => new_hl.get(i).cloned(), + Side::Old(i) => old_hl.get(i).cloned(), + Side::Skip => None, + }) + .collect() +} + +enum JobState { + Request { + hash: u64, + lines: Vec<(String, DiffLineType)>, + path: String, + theme: String, + }, + Response { + hash: u64, + result: Vec>, + }, +} + +/// Async job that runs [`build_highlight`] off the UI thread. +#[derive(Clone, Default)] +pub struct AsyncDiffHighlightJob { + state: Arc>>, +} + +impl AsyncDiffHighlightJob { + /// Creates a job for the given diff. `hash` identifies the diff + /// so the caller can discard results that arrive after the user + /// has navigated to a different file. + pub fn new( + hash: u64, + lines: Vec<(String, DiffLineType)>, + path: String, + theme: String, + ) -> Self { + Self { + state: Arc::new(Mutex::new(Some(JobState::Request { + hash, + lines, + path, + theme, + }))), + } + } + + /// Returns the finished highlight together with the diff `hash` + /// it was computed for, or `None` if the job has not run yet. + pub fn result( + &self, + ) -> Option<(u64, Vec>)> { + if let Ok(mut state) = self.state.lock() { + if let Some(state) = state.take() { + return match state { + JobState::Request { .. } => None, + JobState::Response { hash, result } => { + Some((hash, result)) + } + }; + } + } + + None + } +} + +impl AsyncJob for AsyncDiffHighlightJob { + type Notification = AsyncAppNotification; + type Progress = ProgressPercent; + + fn run( + &mut self, + _params: RunParams, + ) -> asyncgit::Result { + let mut state_mutex = self.state.lock()?; + + if let Some(state) = state_mutex.take() { + *state_mutex = Some(match state { + JobState::Request { + hash, + lines, + path, + theme, + } => { + let result = + build_highlight(&lines, &path, &theme); + JobState::Response { hash, result } + } + JobState::Response { hash, result } => { + JobState::Response { hash, result } + } + }); + } + + Ok(AsyncAppNotification::SyntaxHighlighting( + SyntaxHighlightProgress::Done, + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ui::style::Theme; + + #[test] + fn test_highlight_side_tokenizes_rust_code() { + let theme = Theme::default(); + + let result = highlight_side( + "let x = 42;\n", + "f.rs", + &theme.get_syntax(), + ); + + assert!(!result.is_empty()); + assert!(result[0].len() > 1); + } + + #[test] + fn test_build_highlight_maps_add_line() { + let theme = Theme::default(); + let lines = vec![ + ("@@ hunk @@".to_string(), DiffLineType::Header), + ("let x = 42;".to_string(), DiffLineType::Add), + ]; + + let result = + build_highlight(&lines, "f.rs", &theme.get_syntax()); + + assert_eq!(result.len(), 2); + assert!(result[0].is_none()); + assert!(result[1].as_ref().is_some_and(|v| v.len() > 1)); + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 51322e18ae..1fefc899ee 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -41,6 +41,7 @@ mod commit_details; mod commitlist; mod cred; mod diff; +mod diff_highlight; mod revision_files; mod status_tree; mod syntax_text; diff --git a/src/keys/key_list.rs b/src/keys/key_list.rs index 24a9507a49..41e8d474f4 100644 --- a/src/keys/key_list.rs +++ b/src/keys/key_list.rs @@ -75,6 +75,7 @@ pub struct KeysList { pub status_ignore_file: GituiKeyEvent, pub diff_stage_lines: GituiKeyEvent, pub diff_reset_lines: GituiKeyEvent, + pub diff_toggle_syntax: GituiKeyEvent, pub stashing_save: GituiKeyEvent, pub stashing_toggle_untracked: GituiKeyEvent, pub stashing_toggle_index: GituiKeyEvent, @@ -171,6 +172,7 @@ impl Default for KeysList { status_stage_all: GituiKeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()), status_reset_item: GituiKeyEvent::new(KeyCode::Char('D'), KeyModifiers::SHIFT), diff_reset_lines: GituiKeyEvent::new(KeyCode::Char('d'), KeyModifiers::empty()), + diff_toggle_syntax: GituiKeyEvent::new(KeyCode::Char('X'), KeyModifiers::SHIFT), status_ignore_file: GituiKeyEvent::new(KeyCode::Char('i'), KeyModifiers::empty()), diff_stage_lines: GituiKeyEvent::new(KeyCode::Char('s'), KeyModifiers::empty()), stashing_save: GituiKeyEvent::new(KeyCode::Char('s'), KeyModifiers::empty()), diff --git a/src/options.rs b/src/options.rs index a80e5cb80f..15a4039d35 100644 --- a/src/options.rs +++ b/src/options.rs @@ -16,12 +16,48 @@ use std::{ rc::Rc, }; +#[derive( + Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize, +)] +pub enum DiffHighlightStyle { + Off, + Gutter, + Sign, + #[default] + Tint, +} + +impl DiffHighlightStyle { + pub const fn next(self) -> Self { + match self { + Self::Off => Self::Gutter, + Self::Gutter => Self::Sign, + Self::Sign => Self::Tint, + Self::Tint => Self::Off, + } + } + pub const fn is_on(self) -> bool { + !matches!(self, Self::Off) + } + pub const fn color_gutter(self) -> bool { + matches!(self, Self::Gutter) + } + pub const fn shows_sign(self) -> bool { + matches!(self, Self::Sign) + } + pub const fn shows_tint(self) -> bool { + matches!(self, Self::Tint) + } +} + #[derive(Default, Clone, Serialize, Deserialize)] struct OptionsData { pub tab: usize, pub diff: DiffOptions, pub status_show_untracked: Option, pub commit_msgs: Vec, + #[serde(default)] + pub diff_highlight_style: Option, } const COMMIT_MSG_HISTORY_LENGTH: usize = 20; @@ -107,6 +143,16 @@ impl Options { self.save(); } + pub fn diff_highlight_style(&self) -> DiffHighlightStyle { + self.data.diff_highlight_style.unwrap_or_default() + } + + pub fn diff_cycle_highlight(&mut self) { + let next = self.diff_highlight_style().next(); + self.data.diff_highlight_style = Some(next); + self.save(); + } + pub fn add_commit_msg(&mut self, msg: &str) { self.data.commit_msgs.push(msg.to_owned()); while self.data.commit_msgs.len() > COMMIT_MSG_HISTORY_LENGTH diff --git a/src/strings.rs b/src/strings.rs index 93496cd2ca..55ffb084e8 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -841,6 +841,19 @@ pub mod commands { CMD_GROUP_DIFF, ) } + pub fn diff_toggle_syntax( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Diff highlight [{}]", + key_config + .get_hint(key_config.keys.diff_toggle_syntax), + ), + "cycle diff highlight style (off / gutter / +- / tint)", + CMD_GROUP_DIFF, + ) + } pub fn close_fuzzy_finder( key_config: &SharedKeyConfig, ) -> CommandText { diff --git a/src/ui/style.rs b/src/ui/style.rs index be21f76b9f..9a4afec4e4 100644 --- a/src/ui/style.rs +++ b/src/ui/style.rs @@ -177,6 +177,33 @@ impl Theme { } } + pub fn diff_line_highlight_bg(&self, selected: bool) -> Style { + if selected { + Style::default().bg(self.selection_bg) + } else { + Style::default() + } + } + + pub fn diff_line_tint( + &self, + typ: DiffLineType, + selected: bool, + ) -> Style { + if selected { + return Style::default().bg(self.selection_bg); + } + match typ { + DiffLineType::Add => { + Style::default().bg(Color::Rgb(25, 48, 30)) + } + DiffLineType::Delete => { + Style::default().bg(Color::Rgb(60, 28, 28)) + } + _ => Style::default(), + } + } + pub fn diff_line( &self, typ: DiffLineType, diff --git a/src/ui/syntax_text.rs b/src/ui/syntax_text.rs index 5be2192ea8..e39c4d82de 100644 --- a/src/ui/syntax_text.rs +++ b/src/ui/syntax_text.rs @@ -73,6 +73,28 @@ impl SyntaxText { file_path: &Path, params: &RunParams, syntax: &str, + ) -> asyncgit::Result { + Self::build(text, file_path, Some(params), syntax) + } + + /// Synchronously highlight `text` without async progress + /// reporting — for callers not inside an `AsyncJob` (the diff + /// view). + pub fn new_sync( + text: String, + file_path: &Path, + syntax: &str, + ) -> asyncgit::Result { + Self::build(text, file_path, None, syntax) + } + + fn build( + text: String, + file_path: &Path, + params: Option< + &RunParams, + >, + syntax: &str, ) -> asyncgit::Result { scope_time!("syntax_highlighting"); let mut state = { @@ -120,10 +142,14 @@ impl SyntaxText { total_count, Duration::from_millis(200), ); - params.set_progress(buffer.send_progress())?; - params.send(AsyncAppNotification::SyntaxHighlighting( - SyntaxHighlightProgress::Progress, - ))?; + if let Some(params) = params { + params.set_progress(buffer.send_progress())?; + params.send( + AsyncAppNotification::SyntaxHighlighting( + SyntaxHighlightProgress::Progress, + ), + )?; + } for (number, line) in text.lines().enumerate() { let ops = state @@ -149,13 +175,16 @@ impl SyntaxText { .collect(), }); - if buffer.update(number) { - params.set_progress(buffer.send_progress())?; - params.send( - AsyncAppNotification::SyntaxHighlighting( - SyntaxHighlightProgress::Progress, - ), - )?; + if let Some(params) = params { + if buffer.update(number) { + params + .set_progress(buffer.send_progress())?; + params.send( + AsyncAppNotification::SyntaxHighlighting( + SyntaxHighlightProgress::Progress, + ), + )?; + } } } }