diff --git a/src/lib.rs b/src/lib.rs index b577f52..f7073fc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,7 +30,7 @@ //! //! ``` //! use imara_diff::intern::InternedInput; -//! use imara_diff::{diff, Algorithm, UnifiedDiffBuilder}; +//! use imara_diff::{diff, Algorithm, UnifiedDiffBuilder, unified_diff}; //! //! let before = r#"fn foo() -> Bar { //! let mut foo = 2; @@ -49,7 +49,7 @@ //! "#; //! //! let input = InternedInput::new(before, after); -//! let diff = diff(Algorithm::Histogram, &input, UnifiedDiffBuilder::new(&input)); +//! let diff = diff(Algorithm::Histogram, &input, UnifiedDiffBuilder::new(&input, unified_diff::ContextSize::symmetrical(3))); //! assert_eq!( //! diff, //! r#"@@ -1,5 +1,8 @@ @@ -150,7 +150,7 @@ //! ``` #[cfg(feature = "unified_diff")] -pub use unified_diff::UnifiedDiffBuilder; +pub use unified_diff::_impl::UnifiedDiffBuilder; use crate::intern::{InternedInput, Token, TokenSource}; pub use crate::sink::Sink; @@ -160,7 +160,7 @@ mod myers; pub mod sink; pub mod sources; #[cfg(feature = "unified_diff")] -mod unified_diff; +pub mod unified_diff; mod util; #[cfg(test)] diff --git a/src/tests.rs b/src/tests.rs index 72fc34f..74b3859 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -6,7 +6,7 @@ use expect_test::{expect, expect_file}; use crate::intern::InternedInput; use crate::sink::Counter; -use crate::{diff, Algorithm, UnifiedDiffBuilder}; +use crate::{diff, unified_diff::ContextSize, Algorithm, UnifiedDiffBuilder}; #[test] fn replace() { @@ -28,7 +28,11 @@ fn foo() -> Bar{ let input = InternedInput::new(before, after); for algorithm in Algorithm::ALL { println!("{algorithm:?}"); - let diff = diff(algorithm, &input, UnifiedDiffBuilder::new(&input)); + let diff = diff( + algorithm, + &input, + UnifiedDiffBuilder::new(&input, ContextSize::default()), + ); expect![[r#" @@ -1,5 +1,8 @@ +const TEST: i32 = 0; @@ -55,7 +59,11 @@ fn identical_files() { for algorithm in Algorithm::ALL { println!("{algorithm:?}"); let input = InternedInput::new(file, file); - let diff = diff(algorithm, &input, UnifiedDiffBuilder::new(&input)); + let diff = diff( + algorithm, + &input, + UnifiedDiffBuilder::new(&input, ContextSize::default()), + ); assert_eq!(diff, ""); } } @@ -76,7 +84,11 @@ fn simple_insert() { let mut input = InternedInput::new(before, after); for algorithm in Algorithm::ALL { println!("{algorithm:?}"); - let res = diff(algorithm, &input, UnifiedDiffBuilder::new(&input)); + let res = diff( + algorithm, + &input, + UnifiedDiffBuilder::new(&input, ContextSize::default()), + ); expect![[r#" @@ -1,4 +1,5 @@ fn foo() -> Bar{ @@ -89,7 +101,11 @@ fn simple_insert() { swap(&mut input.before, &mut input.after); - let res = diff(algorithm, &input, UnifiedDiffBuilder::new(&input)); + let res = diff( + algorithm, + &input, + UnifiedDiffBuilder::new(&input, ContextSize::default()), + ); expect![[r#" @@ -1,5 +1,4 @@ fn foo() -> Bar{ @@ -129,8 +145,20 @@ fn hand_checked_udiffs() { let before = read_to_string(path_before).unwrap(); let after = read_to_string(path_after).unwrap(); let input = InternedInput::new(&*before, &*after); - let diff = diff(algorithm, &input, UnifiedDiffBuilder::new(&input)); - expect_file![path_diff].assert_eq(&diff); + let diff_res = diff( + algorithm, + &input, + UnifiedDiffBuilder::new(&input, ContextSize::default()), + ); + expect_file![path_diff].assert_eq(&diff_res); + // test with a context of 5 lines + let path_diff = test_dir.join(format!("{file}.{algorithm:?}_ctx5.diff")); + let diff_res = diff( + algorithm, + &input, + UnifiedDiffBuilder::new(&input, ContextSize::symmetrical(5)), + ); + expect_file![path_diff].assert_eq(&diff_res); } } diff --git a/src/unified_diff.rs b/src/unified_diff.rs index 042bd7b..cbea240 100644 --- a/src/unified_diff.rs +++ b/src/unified_diff.rs @@ -1,138 +1,177 @@ -use std::fmt::{Display, Write}; -use std::ops::Range; - -use crate::intern::{InternedInput, Interner, Token}; -use crate::Sink; - -/// A [`Sink`] that creates a textual diff -/// in the format typically output by git or gnu-diff if the `-u` option is used -pub struct UnifiedDiffBuilder<'a, W, T> -where - W: Write, - T: Display, -{ - before: &'a [Token], - after: &'a [Token], - interner: &'a Interner, - - pos: u32, - before_hunk_start: u32, - after_hunk_start: u32, - before_hunk_len: u32, - after_hunk_len: u32, - - buffer: String, - dst: W, +/// Defines the size of the context printed before and after each change. +/// +/// Similar to the -U option in git diff or gnu-diff. If the context overlaps +/// with previous or next change, the context gets reduced accordingly. +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)] +pub struct ContextSize { + /// Defines the size of the context printed before and after each change. + symmetrical: u32, } -impl<'a, T> UnifiedDiffBuilder<'a, String, T> -where - T: Display, -{ - /// Create a new `UnifiedDiffBuilder` for the given `input`, - /// that will return a [`String`]. - pub fn new(input: &'a InternedInput) -> Self { - Self { - before_hunk_start: 0, - after_hunk_start: 0, - before_hunk_len: 0, - after_hunk_len: 0, - buffer: String::with_capacity(8), - dst: String::new(), - interner: &input.interner, - before: &input.before, - after: &input.after, - pos: 0, - } +impl Default for ContextSize { + fn default() -> Self { + ContextSize::symmetrical(3) } } -impl<'a, W, T> UnifiedDiffBuilder<'a, W, T> -where - W: Write, - T: Display, -{ - /// Create a new `UnifiedDiffBuilder` for the given `input`, - /// that will writes it output to the provided implementation of [`Write`]. - pub fn with_writer(input: &'a InternedInput, writer: W) -> Self { - Self { - before_hunk_start: 0, - after_hunk_start: 0, - before_hunk_len: 0, - after_hunk_len: 0, - buffer: String::with_capacity(8), - dst: writer, - interner: &input.interner, - before: &input.before, - after: &input.after, - pos: 0, - } +/// Instantiation +impl ContextSize { + /// Create a symmetrical context with `n` lines before and after a changed hunk. + pub fn symmetrical(n: u32) -> Self { + ContextSize { symmetrical: n } } +} + +pub(super) mod _impl { + use std::fmt::{Display, Write}; + use std::ops::Range; + + use super::ContextSize; + use crate::intern::{InternedInput, Interner, Token}; + use crate::Sink; + + /// A [`Sink`] that creates a textual diff + /// in the format typically output by git or gnu-diff if the `-u` option is used + pub struct UnifiedDiffBuilder<'a, W, T> + where + W: Write, + T: Display, + { + before: &'a [Token], + after: &'a [Token], + interner: &'a Interner, - fn print_tokens(&mut self, tokens: &[Token], prefix: char) { - for &token in tokens { - writeln!(&mut self.buffer, "{prefix}{}", self.interner[token]).unwrap(); + pos: u32, + before_hunk_start: u32, + after_hunk_start: u32, + before_hunk_len: u32, + after_hunk_len: u32, + /// Symmetrical context before and after the changed hunk. + ctx_size: u32, + + buffer: String, + dst: W, + } + + impl<'a, T> UnifiedDiffBuilder<'a, String, T> + where + T: Display, + { + /// Create a new `UnifiedDiffBuilder` for the given `input`, + /// displaying `context_size` lines of context around each change, + /// that will return a [`String`]. + pub fn new(input: &'a InternedInput, context_size: ContextSize) -> Self { + Self { + before_hunk_start: 0, + after_hunk_start: 0, + before_hunk_len: 0, + after_hunk_len: 0, + buffer: String::with_capacity(8), + dst: String::new(), + interner: &input.interner, + before: &input.before, + after: &input.after, + pos: 0, + ctx_size: context_size.symmetrical, + } } } - fn flush(&mut self) { - if self.before_hunk_len == 0 && self.after_hunk_len == 0 { - return; + impl<'a, W, T> UnifiedDiffBuilder<'a, W, T> + where + W: Write, + T: Display, + { + /// Create a new `UnifiedDiffBuilder` for the given `input`, + /// displaying `context_size` lines of context around each change, + /// that will writes it output to the provided implementation of [`Write`]. + pub fn with_writer( + input: &'a InternedInput, + writer: W, + context_size: Option, + ) -> Self { + Self { + before_hunk_start: 0, + after_hunk_start: 0, + before_hunk_len: 0, + after_hunk_len: 0, + buffer: String::with_capacity(8), + dst: writer, + interner: &input.interner, + before: &input.before, + after: &input.after, + pos: 0, + ctx_size: context_size.unwrap_or(3), + } } - let end = (self.pos + 3).min(self.before.len() as u32); - self.update_pos(end, end); - - writeln!( - &mut self.dst, - "@@ -{},{} +{},{} @@", - self.before_hunk_start + 1, - self.before_hunk_len, - self.after_hunk_start + 1, - self.after_hunk_len, - ) - .unwrap(); - write!(&mut self.dst, "{}", &self.buffer).unwrap(); - self.buffer.clear(); - self.before_hunk_len = 0; - self.after_hunk_len = 0 - } + fn print_tokens(&mut self, tokens: &[Token], prefix: char) { + for &token in tokens { + writeln!(&mut self.buffer, "{prefix}{}", self.interner[token]).unwrap(); + } + } - fn update_pos(&mut self, print_to: u32, move_to: u32) { - self.print_tokens(&self.before[self.pos as usize..print_to as usize], ' '); - let len = print_to - self.pos; - self.pos = move_to; - self.before_hunk_len += len; - self.after_hunk_len += len; - } -} + fn flush(&mut self) { + if self.before_hunk_len == 0 && self.after_hunk_len == 0 { + return; + } -impl Sink for UnifiedDiffBuilder<'_, W, T> -where - W: Write, - T: Display, -{ - type Out = W; + let end = (self.pos + self.ctx_size).min(self.before.len() as u32); + self.update_pos(end, end); - fn process_change(&mut self, before: Range, after: Range) { - if before.start - self.pos > 6 { - self.flush(); - self.pos = before.start - 3; - self.before_hunk_start = self.pos; - self.after_hunk_start = after.start - 3; + writeln!( + &mut self.dst, + "@@ -{},{} +{},{} @@", + self.before_hunk_start + 1, + self.before_hunk_len, + self.after_hunk_start + 1, + self.after_hunk_len, + ) + .unwrap(); + write!(&mut self.dst, "{}", &self.buffer).unwrap(); + self.buffer.clear(); + self.before_hunk_len = 0; + self.after_hunk_len = 0 + } + + fn update_pos(&mut self, print_to: u32, move_to: u32) { + self.print_tokens(&self.before[self.pos as usize..print_to as usize], ' '); + let len = print_to - self.pos; + self.pos = move_to; + self.before_hunk_len += len; + self.after_hunk_len += len; } - self.update_pos(before.start, before.end); - self.before_hunk_len += before.end - before.start; - self.after_hunk_len += after.end - after.start; - self.print_tokens( - &self.before[before.start as usize..before.end as usize], - '-', - ); - self.print_tokens(&self.after[after.start as usize..after.end as usize], '+'); } - fn finish(mut self) -> Self::Out { - self.flush(); - self.dst + impl Sink for UnifiedDiffBuilder<'_, W, T> + where + W: Write, + T: Display, + { + type Out = W; + + fn process_change(&mut self, before: Range, after: Range) { + if ((self.pos == 0) && (before.start - self.pos > self.ctx_size)) + || (before.start - self.pos > 2 * self.ctx_size) + { + self.flush(); + self.pos = before.start - self.ctx_size; + self.before_hunk_start = self.pos; + self.after_hunk_start = after.start - self.ctx_size; + } + self.update_pos(before.start, before.end); + self.before_hunk_len += before.end - before.start; + self.after_hunk_len += after.end - after.start; + self.print_tokens( + &self.before[before.start as usize..before.end as usize], + '-', + ); + self.print_tokens(&self.after[after.start as usize..after.end as usize], '+'); + } + + fn finish(mut self) -> Self::Out { + self.flush(); + self.dst + } } } diff --git a/tests/helix_syntax.rs.Histogram_ctx5.diff b/tests/helix_syntax.rs.Histogram_ctx5.diff new file mode 100644 index 0000000..a08b9e4 --- /dev/null +++ b/tests/helix_syntax.rs.Histogram_ctx5.diff @@ -0,0 +1,255 @@ +@@ -6,17 +6,19 @@ + transaction::{ChangeSet, Operation}, + Rope, RopeSlice, Tendril, + }; + + use arc_swap::{ArcSwap, Guard}; ++use bitflags::bitflags; + use slotmap::{DefaultKey as LayerId, HopSlotMap}; + + use std::{ + borrow::Cow, + cell::RefCell, +- collections::{HashMap, HashSet, VecDeque}, ++ collections::{HashMap, VecDeque}, + fmt, ++ mem::replace, + path::Path, + str::FromStr, + sync::Arc, + }; + +@@ -592,10 +594,11 @@ + pub fn new(source: &Rope, config: Arc, loader: Arc) -> Self { + let root_layer = LanguageLayer { + tree: None, + config, + depth: 0, ++ flags: LayerUpdateFlags::empty(), + ranges: vec![Range { + start_byte: 0, + end_byte: usize::MAX, + start_point: Point::new(0, 0), + end_point: Point::new(usize::MAX, usize::MAX), +@@ -654,13 +657,14 @@ + } else { + Point::new(0, a.column.saturating_sub(b.column)) + } + } + +- for layer in &mut self.layers.values_mut() { ++ for layer in self.layers.values_mut() { + // The root layer always covers the whole range (0..usize::MAX) + if layer.depth == 0 { ++ layer.flags = LayerUpdateFlags::MODIFIED; + continue; + } + + for range in &mut layer.ranges { + // Roughly based on https://github.com/tree-sitter/tree-sitter/blob/ddeaa0c7f534268b35b4f6cb39b52df082754413/lib/src/subtree.c#L691-L720 +@@ -687,10 +691,12 @@ + .saturating_add(range.end_byte - edit.old_end_byte); + range.end_point = point_add( + edit.new_end_position, + point_sub(range.end_point, edit.old_end_position), + ); ++ ++ layer.flags |= LayerUpdateFlags::MOVED; + } + // if the edit starts in the space before and extends into the range + else if edit.start_byte < range.start_byte { + range.start_byte = edit.new_end_byte; + range.start_point = edit.new_end_position; +@@ -701,24 +707,27 @@ + .saturating_add(edit.new_end_byte); + range.end_point = point_add( + edit.new_end_position, + point_sub(range.end_point, edit.old_end_position), + ); ++ layer.flags = LayerUpdateFlags::MODIFIED; + } + // If the edit is an insertion at the start of the tree, shift + else if edit.start_byte == range.start_byte && is_pure_insertion { + range.start_byte = edit.new_end_byte; + range.start_point = edit.new_end_position; ++ layer.flags |= LayerUpdateFlags::MOVED; + } else { + range.end_byte = range + .end_byte + .saturating_sub(edit.old_end_byte) + .saturating_add(edit.new_end_byte); + range.end_point = point_add( + edit.new_end_position, + point_sub(range.end_point, edit.old_end_position), + ); ++ layer.flags = LayerUpdateFlags::MODIFIED; + } + } + } + } + } +@@ -729,31 +738,37 @@ + // TODO: might need to set cursor range + cursor.set_byte_range(0..usize::MAX); + + let source_slice = source.slice(..); + +- let mut touched = HashSet::new(); +- +- // TODO: we should be able to avoid editing & parsing layers with ranges earlier in the document before the edit +- + while let Some(layer_id) = queue.pop_front() { +- // Mark the layer as touched +- touched.insert(layer_id); +- + let layer = &mut self.layers[layer_id]; ++ ++ // Mark the layer as touched ++ layer.flags |= LayerUpdateFlags::TOUCHED; + + // If a tree already exists, notify it of changes. + if let Some(tree) = &mut layer.tree { +- for edit in edits.iter().rev() { +- // Apply the edits in reverse. +- // If we applied them in order then edit 1 would disrupt the positioning of edit 2. +- tree.edit(edit); ++ if layer ++ .flags ++ .intersects(LayerUpdateFlags::MODIFIED | LayerUpdateFlags::MOVED) ++ { ++ for edit in edits.iter().rev() { ++ // Apply the edits in reverse. ++ // If we applied them in order then edit 1 would disrupt the positioning of edit 2. ++ tree.edit(edit); ++ } + } +- } + +- // Re-parse the tree. +- layer.parse(&mut ts_parser.parser, source)?; ++ if layer.flags.contains(LayerUpdateFlags::MODIFIED) { ++ // Re-parse the tree. ++ layer.parse(&mut ts_parser.parser, source)?; ++ } ++ } else { ++ // always parse if this layer has never been parsed before ++ layer.parse(&mut ts_parser.parser, source)?; ++ } + + // Switch to an immutable borrow. + let layer = &self.layers[layer_id]; + + // Process injections. +@@ -853,10 +868,12 @@ + self.layers.insert(LanguageLayer { + tree: None, + config, + depth, + ranges, ++ // set the modified flag to ensure the layer is parsed ++ flags: LayerUpdateFlags::empty(), + }) + }); + + queue.push_back(layer_id); + } +@@ -866,12 +883,15 @@ + } + + // Return the cursor back in the pool. + ts_parser.cursors.push(cursor); + +- // Remove all untouched layers +- self.layers.retain(|id, _| touched.contains(&id)); ++ // Reset all `LayerUpdateFlags` and remove all untouched layers ++ self.layers.retain(|_, layer| { ++ replace(&mut layer.flags, LayerUpdateFlags::empty()) ++ .contains(LayerUpdateFlags::TOUCHED) ++ }); + + Ok(()) + }) + } + +@@ -966,28 +986,41 @@ + // indent_level_for_line + + // TODO: Folding + } + ++bitflags! { ++ /// Flags that track the status of a layer ++ /// in the `Sytaxn::update` function ++ struct LayerUpdateFlags : u32{ ++ const MODIFIED = 0b001; ++ const MOVED = 0b010; ++ const TOUCHED = 0b100; ++ } ++} ++ + #[derive(Debug)] + pub struct LanguageLayer { + // mode + // grammar + pub config: Arc, + pub(crate) tree: Option, + pub ranges: Vec, +- pub depth: usize, ++ pub depth: u32, ++ flags: LayerUpdateFlags, + } + + impl LanguageLayer { + pub fn tree(&self) -> &Tree { + // TODO: no unwrap + self.tree.as_ref().unwrap() + } + + fn parse(&mut self, parser: &mut Parser, source: &Rope) -> Result<(), Error> { +- parser.set_included_ranges(&self.ranges).unwrap(); ++ parser ++ .set_included_ranges(&self.ranges) ++ .map_err(|_| Error::InvalidRanges)?; + + parser + .set_language(self.config.language) + .map_err(|_| Error::InvalidLanguage)?; + +@@ -1133,10 +1166,11 @@ + /// Represents the reason why syntax highlighting failed. + #[derive(Debug, PartialEq, Eq)] + pub enum Error { + Cancelled, + InvalidLanguage, ++ InvalidRanges, + Unknown, + } + + /// Represents a single step in rendering a syntax-highlighted document. + #[derive(Copy, Clone, Debug)] +@@ -1186,11 +1220,11 @@ + byte_offset: usize, + cancellation_flag: Option<&'a AtomicUsize>, + layers: Vec>, + iter_count: usize, + next_event: Option, +- last_highlight_range: Option<(usize, usize, usize)>, ++ last_highlight_range: Option<(usize, usize, u32)>, + } + + // Adapter to convert rope chunks to bytes + pub struct ChunksBytes<'a> { + chunks: ropey::iter::Chunks<'a>, +@@ -1219,11 +1253,11 @@ + cursor: QueryCursor, + captures: iter::Peekable>>, + config: &'a HighlightConfiguration, + highlight_end_stack: Vec, + scope_stack: Vec>, +- depth: usize, ++ depth: u32, + ranges: &'a [Range], + } + + impl<'a> fmt::Debug for HighlightIterLayer<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { diff --git a/tests/helix_syntax.rs.Myers_ctx5.diff b/tests/helix_syntax.rs.Myers_ctx5.diff new file mode 100644 index 0000000..512ba65 --- /dev/null +++ b/tests/helix_syntax.rs.Myers_ctx5.diff @@ -0,0 +1,258 @@ +@@ -6,17 +6,19 @@ + transaction::{ChangeSet, Operation}, + Rope, RopeSlice, Tendril, + }; + + use arc_swap::{ArcSwap, Guard}; ++use bitflags::bitflags; + use slotmap::{DefaultKey as LayerId, HopSlotMap}; + + use std::{ + borrow::Cow, + cell::RefCell, +- collections::{HashMap, HashSet, VecDeque}, ++ collections::{HashMap, VecDeque}, + fmt, ++ mem::replace, + path::Path, + str::FromStr, + sync::Arc, + }; + +@@ -592,10 +594,11 @@ + pub fn new(source: &Rope, config: Arc, loader: Arc) -> Self { + let root_layer = LanguageLayer { + tree: None, + config, + depth: 0, ++ flags: LayerUpdateFlags::empty(), + ranges: vec![Range { + start_byte: 0, + end_byte: usize::MAX, + start_point: Point::new(0, 0), + end_point: Point::new(usize::MAX, usize::MAX), +@@ -654,13 +657,14 @@ + } else { + Point::new(0, a.column.saturating_sub(b.column)) + } + } + +- for layer in &mut self.layers.values_mut() { ++ for layer in self.layers.values_mut() { + // The root layer always covers the whole range (0..usize::MAX) + if layer.depth == 0 { ++ layer.flags = LayerUpdateFlags::MODIFIED; + continue; + } + + for range in &mut layer.ranges { + // Roughly based on https://github.com/tree-sitter/tree-sitter/blob/ddeaa0c7f534268b35b4f6cb39b52df082754413/lib/src/subtree.c#L691-L720 +@@ -687,10 +691,12 @@ + .saturating_add(range.end_byte - edit.old_end_byte); + range.end_point = point_add( + edit.new_end_position, + point_sub(range.end_point, edit.old_end_position), + ); ++ ++ layer.flags |= LayerUpdateFlags::MOVED; + } + // if the edit starts in the space before and extends into the range + else if edit.start_byte < range.start_byte { + range.start_byte = edit.new_end_byte; + range.start_point = edit.new_end_position; +@@ -701,24 +707,27 @@ + .saturating_add(edit.new_end_byte); + range.end_point = point_add( + edit.new_end_position, + point_sub(range.end_point, edit.old_end_position), + ); ++ layer.flags = LayerUpdateFlags::MODIFIED; + } + // If the edit is an insertion at the start of the tree, shift + else if edit.start_byte == range.start_byte && is_pure_insertion { + range.start_byte = edit.new_end_byte; + range.start_point = edit.new_end_position; ++ layer.flags |= LayerUpdateFlags::MOVED; + } else { + range.end_byte = range + .end_byte + .saturating_sub(edit.old_end_byte) + .saturating_add(edit.new_end_byte); + range.end_point = point_add( + edit.new_end_position, + point_sub(range.end_point, edit.old_end_position), + ); ++ layer.flags = LayerUpdateFlags::MODIFIED; + } + } + } + } + } +@@ -728,32 +737,38 @@ + let mut cursor = ts_parser.cursors.pop().unwrap_or_else(QueryCursor::new); + // TODO: might need to set cursor range + cursor.set_byte_range(0..usize::MAX); + + let source_slice = source.slice(..); +- +- let mut touched = HashSet::new(); + +- // TODO: we should be able to avoid editing & parsing layers with ranges earlier in the document before the edit +- + while let Some(layer_id) = queue.pop_front() { +- // Mark the layer as touched +- touched.insert(layer_id); +- + let layer = &mut self.layers[layer_id]; ++ ++ // Mark the layer as touched ++ layer.flags |= LayerUpdateFlags::TOUCHED; + + // If a tree already exists, notify it of changes. + if let Some(tree) = &mut layer.tree { +- for edit in edits.iter().rev() { +- // Apply the edits in reverse. +- // If we applied them in order then edit 1 would disrupt the positioning of edit 2. +- tree.edit(edit); ++ if layer ++ .flags ++ .intersects(LayerUpdateFlags::MODIFIED | LayerUpdateFlags::MOVED) ++ { ++ for edit in edits.iter().rev() { ++ // Apply the edits in reverse. ++ // If we applied them in order then edit 1 would disrupt the positioning of edit 2. ++ tree.edit(edit); ++ } + } +- } + +- // Re-parse the tree. +- layer.parse(&mut ts_parser.parser, source)?; ++ if layer.flags.contains(LayerUpdateFlags::MODIFIED) { ++ // Re-parse the tree. ++ layer.parse(&mut ts_parser.parser, source)?; ++ } ++ } else { ++ // always parse if this layer has never been parsed before ++ layer.parse(&mut ts_parser.parser, source)?; ++ } + + // Switch to an immutable borrow. + let layer = &self.layers[layer_id]; + + // Process injections. +@@ -853,10 +868,12 @@ + self.layers.insert(LanguageLayer { + tree: None, + config, + depth, + ranges, ++ // set the modified flag to ensure the layer is parsed ++ flags: LayerUpdateFlags::empty(), + }) + }); + + queue.push_back(layer_id); + } +@@ -866,12 +883,15 @@ + } + + // Return the cursor back in the pool. + ts_parser.cursors.push(cursor); + +- // Remove all untouched layers +- self.layers.retain(|id, _| touched.contains(&id)); ++ // Reset all `LayerUpdateFlags` and remove all untouched layers ++ self.layers.retain(|_, layer| { ++ replace(&mut layer.flags, LayerUpdateFlags::empty()) ++ .contains(LayerUpdateFlags::TOUCHED) ++ }); + + Ok(()) + }) + } + +@@ -964,30 +984,43 @@ + // suggested_indent_for_line_at_buffer_row + // suggested_indent_for_buffer_row + // indent_level_for_line + + // TODO: Folding ++} ++ ++bitflags! { ++ /// Flags that track the status of a layer ++ /// in the `Sytaxn::update` function ++ struct LayerUpdateFlags : u32{ ++ const MODIFIED = 0b001; ++ const MOVED = 0b010; ++ const TOUCHED = 0b100; ++ } + } + + #[derive(Debug)] + pub struct LanguageLayer { + // mode + // grammar + pub config: Arc, + pub(crate) tree: Option, + pub ranges: Vec, +- pub depth: usize, ++ pub depth: u32, ++ flags: LayerUpdateFlags, + } + + impl LanguageLayer { + pub fn tree(&self) -> &Tree { + // TODO: no unwrap + self.tree.as_ref().unwrap() + } + + fn parse(&mut self, parser: &mut Parser, source: &Rope) -> Result<(), Error> { +- parser.set_included_ranges(&self.ranges).unwrap(); ++ parser ++ .set_included_ranges(&self.ranges) ++ .map_err(|_| Error::InvalidRanges)?; + + parser + .set_language(self.config.language) + .map_err(|_| Error::InvalidLanguage)?; + +@@ -1133,10 +1166,11 @@ + /// Represents the reason why syntax highlighting failed. + #[derive(Debug, PartialEq, Eq)] + pub enum Error { + Cancelled, + InvalidLanguage, ++ InvalidRanges, + Unknown, + } + + /// Represents a single step in rendering a syntax-highlighted document. + #[derive(Copy, Clone, Debug)] +@@ -1186,11 +1220,11 @@ + byte_offset: usize, + cancellation_flag: Option<&'a AtomicUsize>, + layers: Vec>, + iter_count: usize, + next_event: Option, +- last_highlight_range: Option<(usize, usize, usize)>, ++ last_highlight_range: Option<(usize, usize, u32)>, + } + + // Adapter to convert rope chunks to bytes + pub struct ChunksBytes<'a> { + chunks: ropey::iter::Chunks<'a>, +@@ -1219,11 +1253,11 @@ + cursor: QueryCursor, + captures: iter::Peekable>>, + config: &'a HighlightConfiguration, + highlight_end_stack: Vec, + scope_stack: Vec>, +- depth: usize, ++ depth: u32, + ranges: &'a [Range], + } + + impl<'a> fmt::Debug for HighlightIterLayer<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {