diff --git a/crates/phosphor/src/language.rs b/crates/phosphor/src/language.rs index 72945b7..1e23c14 100644 --- a/crates/phosphor/src/language.rs +++ b/crates/phosphor/src/language.rs @@ -12,7 +12,9 @@ use tree_sitter::StreamingIterator; use carbon::{TextByteRange, u32_to_usize_saturating, usize_to_u32_saturating}; use crate::error::{PhosphorError, Result}; -use crate::{HighlightKind, HighlightLineBuffer, HighlightSpan, LanguageId, LanguageMetadata}; +use crate::{ + HighlightKind, HighlightLineBuffer, HighlightSpan, LanguageId, LanguageMetadata, ParsedSyntax, +}; #[derive(Debug)] struct CompiledLanguage { @@ -140,6 +142,16 @@ pub(crate) fn highlight(language: LanguageId, source: &str) -> Result Result { + if !is_parser_available(language) { + return Err(PhosphorError::MissingParser { language }); + } + + let compiled = compiled_language(language)?; + let tree = parse_source(&compiled, source)?; + Ok(ParsedSyntax { language, tree }) +} + pub(crate) fn highlight_text_ranges( language: LanguageId, source: &str, @@ -163,6 +175,24 @@ pub(crate) fn highlight_text_ranges( compact_spans(language, raw_spans) } +pub(crate) fn highlight_text_ranges_with_parse( + parsed: &ParsedSyntax, + source: &str, + byte_ranges: &[TextByteRange], +) -> Result> { + if source.is_empty() || byte_ranges.is_empty() { + return Ok(Vec::new()); + } + let ranges = merged_text_ranges(byte_ranges, source.len()); + if ranges.is_empty() { + return Ok(Vec::new()); + } + + let compiled = compiled_language(parsed.language)?; + let raw_spans = collect_spans_in_ranges(&compiled, &parsed.tree, source, &ranges); + compact_spans(parsed.language, raw_spans) +} + pub(crate) fn highlight_text_lines( language: LanguageId, source: &str, diff --git a/crates/phosphor/src/lib.rs b/crates/phosphor/src/lib.rs index 46ad25b..98892fe 100644 --- a/crates/phosphor/src/lib.rs +++ b/crates/phosphor/src/lib.rs @@ -14,7 +14,7 @@ pub use error::{PhosphorError, Result}; pub use pack::PackInstaller; pub use types::{ HighlightKind, HighlightLine, HighlightLineBuffer, HighlightSpan, HighlightSpanRange, - LanguageId, LanguageMetadata, + LanguageId, LanguageMetadata, ParsedSyntax, }; #[derive(Debug, Default, Clone, Copy)] @@ -77,6 +77,25 @@ impl Highlighter { language::highlight_text_ranges(language, source, byte_ranges) } + pub fn parse_text_store_language( + &self, + language: LanguageId, + text: &TextStore, + ) -> Result { + let source = text.as_str().ok_or(PhosphorError::InvalidUtf8)?; + language::parse(language, source) + } + + pub fn highlight_text_store_language_ranges_with_parse( + &self, + parsed: &ParsedSyntax, + text: &TextStore, + byte_ranges: &[TextByteRange], + ) -> Result> { + let source = text.as_str().ok_or(PhosphorError::InvalidUtf8)?; + language::highlight_text_ranges_with_parse(parsed, source, byte_ranges) + } + pub fn highlight_text_store_language_lines( &self, language: LanguageId, diff --git a/crates/phosphor/src/types.rs b/crates/phosphor/src/types.rs index 88aa5e3..436aef4 100644 --- a/crates/phosphor/src/types.rs +++ b/crates/phosphor/src/types.rs @@ -1,4 +1,5 @@ use carbon::{TextByteRange, u32_to_usize_saturating, usize_to_u32_saturating}; +use tree_sitter as ts; #[repr(u8)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] @@ -110,6 +111,12 @@ impl HighlightLineBuffer { } } +#[derive(Debug, Clone)] +pub struct ParsedSyntax { + pub(crate) language: LanguageId, + pub(crate) tree: ts::Tree, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum LanguageId { Bash, diff --git a/src/apprt/services.rs b/src/apprt/services.rs index 7aefa41..9b1f400 100644 --- a/src/apprt/services.rs +++ b/src/apprt/services.rs @@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Condvar, Mutex}; use std::thread; -use std::time::Duration; +use std::time::{Duration, Instant}; use crate::ai::{self, GenerateRequest, StreamMessage}; use crate::apprt::ProgressReporter; @@ -14,7 +14,7 @@ use crate::core::forge::github::{ PullRequestReviewComment, parse_pr_url, poll_for_token, start_device_flow, }; use crate::core::http; -use crate::core::syntax::annotator::FullFileSyntax; +use crate::core::syntax::annotator::SyntaxTextSource; use crate::core::vcs::discovery; use crate::core::vcs::model::RevisionId; use crate::effects::{ @@ -47,59 +47,65 @@ struct FileSyntaxCacheKey { #[derive(Debug, Default)] struct FileSyntaxCache { - entries: HashMap, + source_entries: HashMap, inflight: HashSet, - bytes: usize, + source_bytes: usize, tick: u64, epoch: u64, } #[derive(Debug)] -struct FileSyntaxCacheEntry { - syntax: Arc, +struct FileSyntaxSourceEntry { + source: FileSyntaxSource, bytes: usize, last_used: u64, } +#[derive(Debug, Clone)] +struct FileSyntaxSource { + text: Arc, + parsed: Option>, +} + impl FileSyntaxCache { - fn get(&mut self, key: &FileSyntaxCacheKey) -> Option> { + fn get_source(&mut self, key: &FileSyntaxCacheKey) -> Option { let tick = self.next_tick(); - let entry = self.entries.get_mut(key)?; + let entry = self.source_entries.get_mut(key)?; entry.last_used = tick; - Some(entry.syntax.clone()) + Some(entry.source.clone()) } - fn insert(&mut self, key: FileSyntaxCacheKey, syntax: Arc) { - const MAX_ENTRIES: usize = 128; - const BYTE_BUDGET: usize = 48 * 1024 * 1024; + fn insert_source(&mut self, key: FileSyntaxCacheKey, source: FileSyntaxSource) { + const MAX_SOURCE_ENTRIES: usize = 64; + const SOURCE_BYTE_BUDGET: usize = 64 * 1024 * 1024; - let bytes = syntax.estimated_bytes().max(1); + let bytes = estimated_text_store_bytes(&source.text).max(1); let tick = self.next_tick(); - if let Some(previous) = self.entries.insert( + if let Some(previous) = self.source_entries.insert( key, - FileSyntaxCacheEntry { - syntax, + FileSyntaxSourceEntry { + source, bytes, last_used: tick, }, ) { - self.bytes = self.bytes.saturating_sub(previous.bytes); + self.source_bytes = self.source_bytes.saturating_sub(previous.bytes); } - self.bytes = self.bytes.saturating_add(bytes); + self.source_bytes = self.source_bytes.saturating_add(bytes); - while self.entries.len() > MAX_ENTRIES - || (self.entries.len() > 1 && self.bytes > BYTE_BUDGET) + while self.source_entries.len() > MAX_SOURCE_ENTRIES + || (self.source_entries.len() > 1 && self.source_bytes > SOURCE_BYTE_BUDGET) { let Some(victim) = self - .entries + .source_entries .iter() .min_by_key(|(_, entry)| entry.last_used) .map(|(key, _)| key.clone()) else { break; }; - if let Some(entry) = self.entries.remove(&victim) { - self.bytes = self.bytes.saturating_sub(entry.bytes); + if let Some(entry) = self.source_entries.remove(&victim) { + self.source_bytes = self.source_bytes.saturating_sub(entry.bytes); } } } @@ -110,13 +116,20 @@ impl FileSyntaxCache { } fn clear(&mut self) { - self.entries.clear(); + self.source_entries.clear(); self.inflight.clear(); - self.bytes = 0; + self.source_bytes = 0; self.epoch = self.epoch.saturating_add(1); } } +fn estimated_text_store_bytes(text: &carbon::TextStore) -> usize { + carbon::u32_to_usize_saturating(text.len()).saturating_add( + carbon::u32_to_usize_saturating(text.line_count()) + .saturating_mul(std::mem::size_of::()), + ) +} + impl AppServices { pub fn new(settings_store: SettingsStore) -> Self { Self { @@ -268,17 +281,19 @@ impl AppServices { if !is_current() { return Vec::new(); } + let total_started = Instant::now(); let Ok(mut repo) = discovery::open_repository(&request.repo_path) else { return Vec::new(); }; + let repo_open_ms = total_started.elapsed().as_millis() as u64; let annotator = crate::core::syntax::DiffSyntaxAnnotator::new(); - let old_syntax = request + let old_source = request .carbon_file .old_path .as_deref() .and_then(|old_path| { - self.cached_file_syntax( + self.cached_file_syntax_source( &mut *repo, request, &request.left_ref, @@ -290,12 +305,12 @@ impl AppServices { if !is_current() { return Vec::new(); } - let new_syntax = request + let new_source = request .carbon_file .new_path .as_deref() .and_then(|new_path| { - self.cached_file_syntax( + self.cached_file_syntax_source( &mut *repo, request, &request.right_ref, @@ -308,17 +323,48 @@ impl AppServices { return Vec::new(); } - annotator.annotate_carbon_full_file_window_from_cache( + let annotate_started = Instant::now(); + let old_text_source = old_source.as_ref().map(|source| SyntaxTextSource { + path: request + .carbon_file + .old_path + .as_deref() + .unwrap_or(&request.path), + text: source.text.as_ref(), + parsed: source.parsed.as_deref(), + }); + let new_text_source = new_source.as_ref().map(|source| SyntaxTextSource { + path: request + .carbon_file + .new_path + .as_deref() + .unwrap_or(&request.path), + text: source.text.as_ref(), + parsed: source.parsed.as_deref(), + }); + let tokens = annotator.annotate_carbon_window_from_text_cache( &request.carbon_file, &request.carbon_expansion, request.file_index, - old_syntax.as_deref(), - new_syntax.as_deref(), + old_text_source, + new_text_source, request.window, - ) + ); + tracing::debug!( + file_index = request.file_index, + path = %request.path, + window_start = request.window.start, + window_end = request.window.end, + repo_open_ms, + annotate_ms = annotate_started.elapsed().as_millis() as u64, + total_ms = total_started.elapsed().as_millis() as u64, + line_updates = tokens.len(), + "syntax viewport load finished" + ); + tokens } - fn cached_file_syntax( + fn cached_file_syntax_source( &self, repo: &mut dyn crate::core::vcs::backend::VcsRepository, request: &LoadFileSyntaxRequest, @@ -326,7 +372,7 @@ impl AppServices { source_path: &str, annotator: &crate::core::syntax::DiffSyntaxAnnotator, is_current: &F, - ) -> Option> + ) -> Option where F: Fn() -> bool, { @@ -345,7 +391,15 @@ impl AppServices { if cache.epoch != key.epoch { return None; } - if let Some(cached) = cache.get(&key) { + if let Some(cached) = cache.get_source(&key) { + tracing::debug!( + file_index = request.file_index, + path = %source_path, + reference = %reference, + bytes = cached.text.len(), + parsed = cached.parsed.is_some(), + "syntax source cache hit" + ); return Some(cached); } if cache.inflight.insert(key.clone()) { @@ -371,6 +425,7 @@ impl AppServices { backend: repo.location().kind, id: reference.to_owned(), }; + let read_started = Instant::now(); let text = match repo.read_file_text(&revision, source_path) { Ok(text) => text, Err(_) => { @@ -381,6 +436,7 @@ impl AppServices { return None; } }; + let read_ms = read_started.elapsed().as_millis() as u64; if !is_current() { if let Ok(mut cache) = self.syntax_cache.lock() { cache.inflight.remove(&key); @@ -388,7 +444,13 @@ impl AppServices { } return None; } - let syntax = Arc::new(annotator.highlight_full_text_store(source_path, &text)); + let parse_started = Instant::now(); + let parsed = annotator.parse_text_store(source_path, &text).map(Arc::new); + let parse_ms = parse_started.elapsed().as_millis() as u64; + let source = FileSyntaxSource { + text: Arc::new(text), + parsed, + }; match self.syntax_cache.lock() { Ok(mut cache) => { cache.inflight.remove(&key); @@ -396,12 +458,22 @@ impl AppServices { self.syntax_cache_ready.notify_all(); return None; } - cache.insert(key, syntax.clone()); + cache.insert_source(key, source.clone()); self.syntax_cache_ready.notify_all(); } Err(_) => self.syntax_cache_ready.notify_all(), } - Some(syntax) + tracing::debug!( + file_index = request.file_index, + path = %source_path, + reference = %reference, + read_ms, + parse_ms, + bytes = source.text.len(), + parsed = source.parsed.is_some(), + "syntax source cache populated" + ); + Some(source) } pub fn clear_file_syntax_cache(&self) { diff --git a/src/core/syntax/annotator.rs b/src/core/syntax/annotator.rs index d1800b4..e7c59a6 100644 --- a/src/core/syntax/annotator.rs +++ b/src/core/syntax/annotator.rs @@ -1,6 +1,7 @@ use crate::core::syntax::Highlighter; use crate::core::text::DiffTokenSpan; -use carbon::{LineId, TextStore}; +use carbon::{LineId, TextByteRange, TextStore}; +use phosphor::ParsedSyntax; #[derive(Debug, Clone, Copy)] struct LineRef { @@ -62,6 +63,13 @@ impl FullFileSyntax { } } +#[derive(Debug, Clone, Copy)] +pub struct SyntaxTextSource<'a> { + pub path: &'a str, + pub text: &'a TextStore, + pub parsed: Option<&'a ParsedSyntax>, +} + #[derive(Debug)] pub struct DiffSyntaxAnnotator { highlighter: Highlighter, @@ -111,6 +119,46 @@ impl DiffSyntaxAnnotator { } } + pub fn parse_text_store(&self, path: &str, text: &TextStore) -> Option { + let language = self.highlighter.resolve_language(path); + match self.highlighter.parse_text_store_resolved(language, text) { + Ok(parsed) => parsed, + Err(error) => { + tracing::warn!( + path = %path, + ?language, + %error, + "syntax parse failed" + ); + None + } + } + } + + pub fn annotate_carbon_window_from_text_cache( + &self, + file: &carbon::FileDiff, + expansion: &carbon::ExpansionState, + file_index: usize, + old_source: Option>, + new_source: Option>, + window: SyntaxRowWindow, + ) -> Vec { + if file.is_binary || window.end <= window.start { + return Vec::new(); + } + + let (old_refs, new_refs) = build_carbon_full_file_refs(file, expansion, file_index, window); + let mut out = Vec::new(); + if let Some(source) = old_source { + out.extend(self.annotate_text_source_ranges(source, &old_refs)); + } + if let Some(source) = new_source { + out.extend(self.annotate_text_source_ranges(source, &new_refs)); + } + out + } + pub fn annotate_carbon_full_file_window_from_cache( &self, file: &carbon::FileDiff, @@ -136,6 +184,45 @@ impl DiffSyntaxAnnotator { } out } + + fn annotate_text_source_ranges( + &self, + source: SyntaxTextSource<'_>, + refs: &[LineRef], + ) -> Vec { + if refs.is_empty() { + return Vec::new(); + } + + let (byte_refs, ranges) = byte_refs_and_ranges_for_text_refs(refs, source.text); + if ranges.is_empty() { + return Vec::new(); + } + + let tokens = if let Some(parsed) = source.parsed { + self.highlighter + .highlight_text_store_ranges_with_parse(parsed, source.text, &ranges) + } else { + let Some(text) = source.text.as_str() else { + return Vec::new(); + }; + let language = self.highlighter.resolve_language(source.path); + self.highlighter + .highlight_resolved_ranges(language, text, &ranges) + }; + + match tokens { + Ok(tokens) => collect_line_tokens(&tokens, &byte_refs), + Err(error) => { + tracing::warn!( + path = %source.path, + %error, + "syntax range highlight failed" + ); + Vec::new() + } + } + } } fn build_carbon_full_file_refs( @@ -232,6 +319,30 @@ fn byte_refs_for_cached_refs(refs: &[LineRef], syntax: &FullFileSyntax) -> Vec (Vec, Vec) { + let mut byte_refs = Vec::with_capacity(refs.len()); + let mut ranges = Vec::with_capacity(refs.len()); + for reference in refs { + let line_index = reference.content_offset; + let Some(range) = text.line_range(LineId(usize_to_u32_saturating(line_index))) else { + continue; + }; + ranges.push(range); + byte_refs.push(LineRef { + hunk_index: reference.hunk_index, + line_index: reference.line_index, + side: reference.side, + source_index: reference.source_index, + content_offset: u32_to_usize_saturating(range.start), + content_len: u32_to_usize_saturating(range.len), + }); + } + (byte_refs, ranges) +} + fn usize_to_u32_saturating(value: usize) -> u32 { u32::try_from(value).unwrap_or(u32::MAX) } diff --git a/src/core/syntax/highlighter.rs b/src/core/syntax/highlighter.rs index d916c63..1b26417 100644 --- a/src/core/syntax/highlighter.rs +++ b/src/core/syntax/highlighter.rs @@ -5,7 +5,7 @@ use crate::core::text::{DiffTokenSpan, SyntaxTokenKind}; use carbon::TextStore; use phosphor::{ HighlightKind, HighlightSpan, Highlighter as PhosphorHighlighter, - LanguageId as PhosphorLanguageId, TextByteRange, + LanguageId as PhosphorLanguageId, ParsedSyntax, TextByteRange, }; #[derive(Debug)] @@ -65,6 +65,23 @@ impl Highlighter { self.highlight_resolved(language, source) } + pub fn parse_text_store_resolved( + &self, + language: Option, + text: &TextStore, + ) -> Result> { + let Some(language) = language else { + return Ok(None); + }; + if !self.inner.is_parser_available(language) { + return Ok(None); + } + self.inner + .parse_text_store_language(language, text) + .map(Some) + .map_err(|error| DiffyError::Syntax(error.to_string())) + } + pub fn highlight_resolved_ranges( &self, language: Option, @@ -82,6 +99,18 @@ impl Highlighter { .map(|spans| spans.into_iter().map(map_span).collect()) .map_err(|error| DiffyError::Syntax(error.to_string())) } + + pub fn highlight_text_store_ranges_with_parse( + &self, + parsed: &ParsedSyntax, + text: &TextStore, + byte_ranges: &[TextByteRange], + ) -> Result> { + self.inner + .highlight_text_store_language_ranges_with_parse(parsed, text, byte_ranges) + .map(|spans| spans.into_iter().map(map_span).collect()) + .map_err(|error| DiffyError::Syntax(error.to_string())) + } } fn map_span(span: HighlightSpan) -> DiffTokenSpan { diff --git a/src/ui/editor/render_doc.rs b/src/ui/editor/render_doc.rs index c1148f4..078219e 100644 --- a/src/ui/editor/render_doc.rs +++ b/src/ui/editor/render_doc.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use crate::core::text::{ChangeIntensity, DiffTokenSpan, SyntaxTokenKind, TokenBuffer, TokenRange}; @@ -388,6 +388,106 @@ pub fn build_render_doc_from_carbon( build_render_doc_from_carbon_rows(carbon_file, file_index, expansion, overlays, token_buffer) } +pub fn refresh_render_doc_syntax_from_carbon( + doc: &mut RenderDoc, + carbon_file: &carbon::FileDiff, + expansion: &carbon::ExpansionState, + overlays: &CarbonStyleOverlays, + token_buffer: &TokenBuffer, + updated: &[CarbonLineKey], +) { + if updated.is_empty() { + return; + } + + let updated = updated.iter().copied().collect::>(); + let mut doc_index = 1usize; + carbon::project_file( + carbon_file, + carbon::ProjectionOptions { + mode: carbon::ProjectionMode::Unified, + collapsed_context_threshold: 0, + include_hunk_headers: true, + }, + expansion, + |row| { + if row.kind == carbon::ProjectionRowKind::ContextGap { + return; + } + let current_index = doc_index; + doc_index = doc_index.saturating_add(1); + if current_index >= doc.lines.len() { + return; + } + + if row_key(row, carbon::DiffSide::Old).is_some_and(|key| updated.contains(&key)) { + refresh_render_doc_side_from_carbon( + doc, + current_index, + carbon_file, + row, + carbon::DiffSide::Old, + overlays, + token_buffer, + ); + } + if row_key(row, carbon::DiffSide::New).is_some_and(|key| updated.contains(&key)) { + refresh_render_doc_side_from_carbon( + doc, + current_index, + carbon_file, + row, + carbon::DiffSide::New, + overlays, + token_buffer, + ); + } + }, + ); +} + +fn row_key(row: carbon::ProjectionRow, side: carbon::DiffSide) -> Option { + let hunk_id = row.hunk_id?.0; + let source_index = match side { + carbon::DiffSide::Old => row.old_index?, + carbon::DiffSide::New => row.new_index?, + }; + Some(CarbonLineKey { + hunk_id, + side, + source_index, + }) +} + +fn refresh_render_doc_side_from_carbon( + doc: &mut RenderDoc, + line_index: usize, + carbon_file: &carbon::FileDiff, + row: carbon::ProjectionRow, + side: carbon::DiffSide, + overlays: &CarbonStyleOverlays, + token_buffer: &TokenBuffer, +) { + let Some(source) = carbon_line_source_from_row(carbon_file, row, side, overlays, token_buffer) + else { + return; + }; + let runs = append_style_runs_with_carbon( + &mut doc.style_runs, + source.text, + source.syntax, + source.core_change, + source.carbon_change, + ); + let Some(line) = doc.lines.get_mut(line_index) else { + return; + }; + match side { + carbon::DiffSide::Old => line.left_runs = runs, + carbon::DiffSide::New => line.right_runs = runs, + } +} + fn build_render_doc_from_carbon_rows( carbon_file: &carbon::FileDiff, file_index: usize, @@ -1299,8 +1399,9 @@ fn carbon_projection_capacity(file: &carbon::FileDiff) -> usize { #[cfg(test)] mod tests { use super::{ - CarbonStyleOverlays, INVALID_U32, RENDER_FLAG_STRUCTURAL, RenderDoc, RenderRowKind, - STYLE_FLAG_CHANGE, build_render_doc_from_carbon, + CarbonLineKey, CarbonStyleOverlays, INVALID_U32, RENDER_FLAG_STRUCTURAL, RenderDoc, + RenderRowKind, STYLE_FLAG_CHANGE, build_render_doc_from_carbon, + refresh_render_doc_syntax_from_carbon, }; use crate::core::text::{DiffTokenSpan, SyntaxTokenKind, TokenBuffer}; @@ -1553,6 +1654,58 @@ diff --git a/src/lib.rs b/src/lib.rs assert_eq!(doc.line_text(doc.lines[5].right_text), "new 4"); } + #[test] + fn syntax_refresh_updates_only_targeted_line_runs() { + let mut token_buffer = TokenBuffer::default(); + let file = carbon::parse_unified_patch( + "\ +diff --git a/src/lib.rs b/src/lib.rs +--- a/src/lib.rs ++++ b/src/lib.rs +@@ -1,2 +1,2 @@ + fn main() { +- old(); ++ new(); + } +", + ) + .unwrap() + .files + .into_iter() + .next() + .unwrap(); + let mut overlays = CarbonStyleOverlays::default(); + let mut doc = carbon_doc(&file, &overlays, &token_buffer); + let original_style_run_len = doc.style_runs.len(); + + let syntax = token_buffer.append(&[DiffTokenSpan { + offset: 4, + length: 3, + kind: SyntaxTokenKind::Function, + ..DiffTokenSpan::default() + }]); + overlays.insert_syntax(0, carbon::DiffSide::New, 1, syntax); + + refresh_render_doc_syntax_from_carbon( + &mut doc, + &file, + &carbon::ExpansionState::default(), + &overlays, + &token_buffer, + &[CarbonLineKey { + hunk_id: 0, + side: carbon::DiffSide::New, + source_index: 1, + }], + ); + + assert!(doc.style_runs.len() > original_style_run_len); + assert_eq!( + doc.line_runs(doc.lines[4].right_runs)[1].style_id, + SyntaxTokenKind::Function as u16 + ); + } + #[test] fn missing_side_uses_invalid_sentinel() { let token_buffer = TokenBuffer::default(); diff --git a/src/ui/state/mod.rs b/src/ui/state/mod.rs index 3e5fb2c..dad68de 100644 --- a/src/ui/state/mod.rs +++ b/src/ui/state/mod.rs @@ -62,7 +62,8 @@ use crate::platform::secrets::AiKeyKind; use crate::platform::startup::StartupOptions; use crate::ui::design::{Sp, Sz}; use crate::ui::editor::render_doc::{ - CarbonStyleOverlays, RenderDoc, build_placeholder_render_doc, build_render_doc_from_carbon, + CarbonLineKey, CarbonStyleOverlays, RenderDoc, build_placeholder_render_doc, + build_render_doc_from_carbon, refresh_render_doc_syntax_from_carbon, }; use crate::ui::editor::state::{EditorState, EditorStateStore, SearchMatch}; use crate::ui::icons::lucide; @@ -693,16 +694,24 @@ fn apply_syntax_tokens_to_file( carbon_overlays: &mut CarbonStyleOverlays, token_buffer: &mut TokenBuffer, updates: &[SyntaxLineTokens], -) { +) -> Vec { + let mut changed = Vec::new(); for update in updates { if let (Some(side), Some(source_index)) = (update.side, update.source_index) { if update.tokens.is_empty() { continue; } let range = token_buffer.append(&update.tokens); - carbon_overlays.insert_syntax(update.hunk_index as u32, side, source_index, range); + let key = CarbonLineKey { + hunk_id: update.hunk_index as u32, + side, + source_index, + }; + carbon_overlays.insert_syntax(key.hunk_id, key.side, key.source_index, range); + changed.push(key); } } + changed } fn active_file_matches_language( @@ -5094,18 +5103,21 @@ impl AppState { return; } push_syntax_covered_window(&mut active.syntax_covered, payload.window); - apply_syntax_tokens_to_file( + let changed = apply_syntax_tokens_to_file( &mut active.carbon_overlays, &mut active.token_buffer, &payload.tokens, ); - active.render_doc = Arc::new(build_render_doc_from_carbon( - &active.carbon_file, - active.index, - &active.carbon_expansion, - &active.carbon_overlays, - &active.token_buffer, - )); + if !changed.is_empty() { + refresh_render_doc_syntax_from_carbon( + Arc::make_mut(&mut active.render_doc), + &active.carbon_file, + &active.carbon_expansion, + &active.carbon_overlays, + &active.token_buffer, + &changed, + ); + } applied_file = Some(active.clone()); applied_active = true; }); @@ -5143,18 +5155,21 @@ impl AppState { return; } push_syntax_covered_window(&mut active.syntax_covered, payload.window); - apply_syntax_tokens_to_file( + let changed = apply_syntax_tokens_to_file( &mut active.carbon_overlays, &mut active.token_buffer, &payload.tokens, ); - active.render_doc = Arc::new(build_render_doc_from_carbon( - &active.carbon_file, - active.index, - &active.carbon_expansion, - &active.carbon_overlays, - &active.token_buffer, - )); + if !changed.is_empty() { + refresh_render_doc_syntax_from_carbon( + Arc::make_mut(&mut active.render_doc), + &active.carbon_file, + &active.carbon_expansion, + &active.carbon_overlays, + &active.token_buffer, + &changed, + ); + } applied_file = Some(active.clone()); }); }