From 0a6293bcda5e6d4027699e90b5151efcdd16e0f8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 18 Nov 2021 14:08:21 +0100 Subject: [PATCH] Support highlighting in blocks Co-Authored-By: Nathan Sobo --- crates/editor/src/display_map.rs | 67 ++++++++++++---------- crates/editor/src/display_map/block_map.rs | 19 +++--- crates/editor/src/display_map/fold_map.rs | 23 ++++---- crates/editor/src/display_map/tab_map.rs | 21 ++++--- crates/editor/src/display_map/wrap_map.rs | 11 ++-- crates/editor/src/element.rs | 8 +-- crates/editor/src/lib.rs | 10 +++- crates/gpui/src/fonts.rs | 2 +- crates/language/src/lib.rs | 29 ++++++---- crates/language/src/tests.rs | 2 +- 10 files changed, 105 insertions(+), 87 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 5d90c36d303d3358c5a11d70d8d6539f187817af..61fe1fa00a36069cf26f8d729441406597facdba 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -12,6 +12,7 @@ use language::{Anchor, Buffer, Point, ToOffset, ToPoint}; use std::{collections::HashSet, ops::Range}; use sum_tree::Bias; use tab_map::TabMap; +use theme::SyntaxTheme; use wrap_map::WrapMap; pub use block_map::{BlockDisposition, BlockProperties, BufferRows, Chunks}; @@ -230,12 +231,16 @@ impl DisplayMapSnapshot { pub fn text_chunks(&self, display_row: u32) -> impl Iterator { self.blocks_snapshot - .chunks(display_row..self.max_point().row() + 1, false) + .chunks(display_row..self.max_point().row() + 1, None) .map(|h| h.text) } - pub fn chunks(&mut self, display_rows: Range) -> block_map::Chunks { - self.blocks_snapshot.chunks(display_rows, true) + pub fn chunks<'a>( + &'a self, + display_rows: Range, + theme: Option<&'a SyntaxTheme>, + ) -> block_map::Chunks<'a> { + self.blocks_snapshot.chunks(display_rows, theme) } pub fn chars_at<'a>(&'a self, point: DisplayPoint) -> impl Iterator + 'a { @@ -736,8 +741,8 @@ mod tests { .unindent(); let theme = SyntaxTheme::new(vec![ - ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()), - ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()), + ("mod.body".to_string(), Color::red().into()), + ("fn.name".to_string(), Color::blue().into()), ]); let lang = Arc::new( Language::new( @@ -776,19 +781,19 @@ mod tests { cx.update(|cx| chunks(0..5, &map, &theme, cx)), vec![ ("fn ".to_string(), None), - ("outer".to_string(), Some("fn.name")), + ("outer".to_string(), Some(Color::blue())), ("() {}\n\nmod module ".to_string(), None), - ("{\n fn ".to_string(), Some("mod.body")), - ("inner".to_string(), Some("fn.name")), - ("() {}\n}".to_string(), Some("mod.body")), + ("{\n fn ".to_string(), Some(Color::red())), + ("inner".to_string(), Some(Color::blue())), + ("() {}\n}".to_string(), Some(Color::red())), ] ); assert_eq!( cx.update(|cx| chunks(3..5, &map, &theme, cx)), vec![ - (" fn ".to_string(), Some("mod.body")), - ("inner".to_string(), Some("fn.name")), - ("() {}\n}".to_string(), Some("mod.body")), + (" fn ".to_string(), Some(Color::red())), + ("inner".to_string(), Some(Color::blue())), + ("() {}\n}".to_string(), Some(Color::red())), ] ); @@ -799,11 +804,11 @@ mod tests { cx.update(|cx| chunks(0..2, &map, &theme, cx)), vec![ ("fn ".to_string(), None), - ("out".to_string(), Some("fn.name")), + ("out".to_string(), Some(Color::blue())), ("…".to_string(), None), - (" fn ".to_string(), Some("mod.body")), - ("inner".to_string(), Some("fn.name")), - ("() {}\n}".to_string(), Some("mod.body")), + (" fn ".to_string(), Some(Color::red())), + ("inner".to_string(), Some(Color::blue())), + ("() {}\n}".to_string(), Some(Color::red())), ] ); } @@ -823,8 +828,8 @@ mod tests { .unindent(); let theme = SyntaxTheme::new(vec![ - ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()), - ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()), + ("mod.body".to_string(), Color::red().into()), + ("fn.name".to_string(), Color::blue().into()), ]); let lang = Arc::new( Language::new( @@ -864,7 +869,7 @@ mod tests { cx.update(|cx| chunks(0..5, &map, &theme, cx)), [ ("fn \n".to_string(), None), - ("oute\nr".to_string(), Some("fn.name")), + ("oute\nr".to_string(), Some(Color::blue())), ("() \n{}\n\n".to_string(), None), ] ); @@ -879,10 +884,10 @@ mod tests { assert_eq!( cx.update(|cx| chunks(1..4, &map, &theme, cx)), [ - ("out".to_string(), Some("fn.name")), + ("out".to_string(), Some(Color::blue())), ("…\n".to_string(), None), - (" \nfn ".to_string(), Some("mod.body")), - ("i\n".to_string(), Some("fn.name")) + (" \nfn ".to_string(), Some(Color::red())), + ("i\n".to_string(), Some(Color::blue())) ] ); } @@ -1018,19 +1023,19 @@ mod tests { map: &ModelHandle, theme: &'a SyntaxTheme, cx: &mut MutableAppContext, - ) -> Vec<(String, Option<&'a str>)> { - let mut snapshot = map.update(cx, |map, cx| map.snapshot(cx)); - let mut chunks: Vec<(String, Option<&str>)> = Vec::new(); - for chunk in snapshot.chunks(rows) { - let style_name = chunk.highlight_id.name(theme); - if let Some((last_chunk, last_style_name)) = chunks.last_mut() { - if style_name == *last_style_name { + ) -> Vec<(String, Option)> { + let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); + let mut chunks: Vec<(String, Option)> = Vec::new(); + for chunk in snapshot.chunks(rows, Some(theme)) { + let color = chunk.highlight_style.map(|s| s.color); + if let Some((last_chunk, last_color)) = chunks.last_mut() { + if color == *last_color { last_chunk.push_str(chunk.text); } else { - chunks.push((chunk.text.to_string(), style_name)); + chunks.push((chunk.text.to_string(), color)); } } else { - chunks.push((chunk.text.to_string(), style_name)); + chunks.push((chunk.text.to_string(), color)); } } chunks diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index f9731662eef9aa4c1f250667f68bc457df3e120f..c95520a41d7b872c5ea3ddbea1c05354adf73a23 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -15,6 +15,7 @@ use std::{ }, }; use sum_tree::SumTree; +use theme::SyntaxTheme; pub struct BlockMap { buffer: ModelHandle, @@ -459,12 +460,12 @@ impl<'a> BlockMapWriter<'a> { impl BlockSnapshot { #[cfg(test)] fn text(&mut self) -> String { - self.chunks(0..self.transforms.summary().output_rows, false) + self.chunks(0..self.transforms.summary().output_rows, None) .map(|chunk| chunk.text) .collect() } - pub fn chunks(&self, rows: Range, highlights: bool) -> Chunks { + pub fn chunks<'a>(&'a self, rows: Range, theme: Option<&'a SyntaxTheme>) -> Chunks<'a> { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); let input_end = { @@ -492,9 +493,7 @@ impl BlockSnapshot { cursor.start().1 .0 + overshoot }; Chunks { - input_chunks: self - .wrap_snapshot - .chunks(input_start..input_end, highlights), + input_chunks: self.wrap_snapshot.chunks(input_start..input_end, theme), input_chunk: Default::default(), block_chunks: None, transforms: cursor, @@ -785,9 +784,9 @@ impl<'a> Iterator for BlockChunks<'a> { let chunk = self.chunk?; let mut chunk_len = chunk.len(); - // let mut highlight_style = None; - if let Some((run_len, _)) = self.runs.peek() { - // highlight_style = Some(style.clone()); + let mut highlight_style = None; + if let Some((run_len, style)) = self.runs.peek() { + highlight_style = Some(style.clone()); let run_end_in_chunk = self.run_start + run_len - self.offset; if run_end_in_chunk <= chunk_len { chunk_len = run_end_in_chunk; @@ -806,7 +805,7 @@ impl<'a> Iterator for BlockChunks<'a> { Some(Chunk { text: chunk, - highlight_id: Default::default(), + highlight_style, diagnostic: None, }) } @@ -1314,7 +1313,7 @@ mod tests { for start_row in 0..expected_row_count { let expected_text = expected_lines[start_row..].join("\n"); let actual_text = blocks_snapshot - .chunks(start_row as u32..expected_row_count as u32, false) + .chunks(start_row as u32..expected_row_count as u32, None) .map(|chunk| chunk.text) .collect::(); assert_eq!( diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 2cb0f215bf003b7ded38a803b23a8f2cd19a2b0e..26d3ff3a7d559e7959de5cb105e47daeeb03059e 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1,7 +1,5 @@ use gpui::{AppContext, ModelHandle}; -use language::{ - Anchor, AnchorRangeExt, Buffer, Chunk, HighlightId, Point, PointUtf16, TextSummary, ToOffset, -}; +use language::{Anchor, AnchorRangeExt, Buffer, Chunk, Point, PointUtf16, TextSummary, ToOffset}; use parking_lot::Mutex; use std::{ cmp::{self, Ordering}, @@ -10,6 +8,7 @@ use std::{ sync::atomic::{AtomicUsize, Ordering::SeqCst}, }; use sum_tree::{Bias, Cursor, FilterCursor, SumTree}; +use theme::SyntaxTheme; pub trait ToFoldPoint { fn to_fold_point(&self, snapshot: &Snapshot, bias: Bias) -> FoldPoint; @@ -498,7 +497,7 @@ pub struct Snapshot { impl Snapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(FoldOffset(0)..self.len(), false) + self.chunks(FoldOffset(0)..self.len(), None) .map(|c| c.text) .collect() } @@ -630,11 +629,15 @@ impl Snapshot { pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator { let start = start.to_offset(self); - self.chunks(start..self.len(), false) + self.chunks(start..self.len(), None) .flat_map(|chunk| chunk.text.chars()) } - pub fn chunks(&self, range: Range, enable_highlights: bool) -> Chunks { + pub fn chunks<'a>( + &'a self, + range: Range, + theme: Option<&'a SyntaxTheme>, + ) -> Chunks<'a> { let mut transform_cursor = self.transforms.cursor::<(FoldOffset, usize)>(); transform_cursor.seek(&range.end, Bias::Right, &()); @@ -647,9 +650,7 @@ impl Snapshot { Chunks { transform_cursor, - buffer_chunks: self - .buffer_snapshot - .chunks(buffer_start..buffer_end, enable_highlights), + buffer_chunks: self.buffer_snapshot.chunks(buffer_start..buffer_end, theme), buffer_chunk: None, buffer_offset: buffer_start, output_offset: range.start.0, @@ -974,7 +975,7 @@ impl<'a> Iterator for Chunks<'a> { self.output_offset += output_text.len(); return Some(Chunk { text: output_text, - highlight_id: HighlightId::default(), + highlight_style: None, diagnostic: None, }); } @@ -1384,7 +1385,7 @@ mod tests { log::info!("slicing {:?}..{:?} (text: {:?})", start, end, text); assert_eq!( snapshot - .chunks(start..end, false) + .chunks(start..end, None) .map(|c| c.text) .collect::(), text, diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index 33f8d692a2959e5f3e973c55f7a2c55a1a51fc28..675ce9132ebdf90e786a95bc1c7c97615cd78ff3 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -4,6 +4,7 @@ use language::{rope, Chunk}; use parking_lot::Mutex; use std::{cmp, mem, ops::Range}; use sum_tree::Bias; +use theme::SyntaxTheme; pub struct TabMap(Mutex); @@ -33,7 +34,7 @@ impl TabMap { let mut delta = 0; for chunk in old_snapshot .fold_snapshot - .chunks(fold_edit.old_bytes.end..max_offset, false) + .chunks(fold_edit.old_bytes.end..max_offset, None) { let patterns: &[_] = &['\t', '\n']; if let Some(ix) = chunk.text.find(patterns) { @@ -116,7 +117,7 @@ impl Snapshot { self.max_point() }; for c in self - .chunks(range.start..line_end, false) + .chunks(range.start..line_end, None) .flat_map(|chunk| chunk.text.chars()) { if c == '\n' { @@ -130,7 +131,7 @@ impl Snapshot { last_line_chars = first_line_chars; } else { for _ in self - .chunks(TabPoint::new(range.end.row(), 0)..range.end, false) + .chunks(TabPoint::new(range.end.row(), 0)..range.end, None) .flat_map(|chunk| chunk.text.chars()) { last_line_chars += 1; @@ -150,7 +151,11 @@ impl Snapshot { self.fold_snapshot.version } - pub fn chunks(&self, range: Range, highlights: bool) -> Chunks { + pub fn chunks<'a>( + &'a self, + range: Range, + theme: Option<&'a SyntaxTheme>, + ) -> Chunks<'a> { let (input_start, expanded_char_column, to_next_stop) = self.to_fold_point(range.start, Bias::Left); let input_start = input_start.to_offset(&self.fold_snapshot); @@ -165,9 +170,7 @@ impl Snapshot { }; Chunks { - fold_chunks: self - .fold_snapshot - .chunks(input_start..input_end, highlights), + fold_chunks: self.fold_snapshot.chunks(input_start..input_end, theme), column: expanded_char_column, output_position: range.start.0, max_output_position: range.end.0, @@ -186,7 +189,7 @@ impl Snapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(TabPoint::zero()..self.max_point(), false) + self.chunks(TabPoint::zero()..self.max_point(), None) .map(|chunk| chunk.text) .collect() } @@ -502,7 +505,7 @@ mod tests { assert_eq!( expected_text, tabs_snapshot - .chunks(start..end, false) + .chunks(start..end, None) .map(|c| c.text) .collect::() ); diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 4efe1ce47b8cf18bb04a63c7f48d6b40e957c950..70afe9d892bca1d0de896d25553d5c60cae9f646 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -12,6 +12,7 @@ use lazy_static::lazy_static; use smol::future::yield_now; use std::{collections::VecDeque, mem, ops::Range, time::Duration}; use sum_tree::{Bias, Cursor, SumTree}; +use theme::SyntaxTheme; pub use super::tab_map::TextSummary; pub type Edit = buffer::Edit; @@ -427,7 +428,7 @@ impl Snapshot { let mut remaining = None; let mut chunks = new_tab_snapshot.chunks( TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(), - false, + None, ); let mut edit_transforms = Vec::::new(); for _ in edit.new_rows.start..edit.new_rows.end { @@ -553,11 +554,11 @@ impl Snapshot { } pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { - self.chunks(wrap_row..self.max_point().row() + 1, false) + self.chunks(wrap_row..self.max_point().row() + 1, None) .map(|h| h.text) } - pub fn chunks(&self, rows: Range, highlights: bool) -> Chunks { + pub fn chunks<'a>(&'a self, rows: Range, theme: Option<&'a SyntaxTheme>) -> Chunks<'a> { let output_start = WrapPoint::new(rows.start, 0); let output_end = WrapPoint::new(rows.end, 0); let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(); @@ -570,7 +571,7 @@ impl Snapshot { .to_tab_point(output_end) .min(self.tab_snapshot.max_point()); Chunks { - input_chunks: self.tab_snapshot.chunks(input_start..input_end, highlights), + input_chunks: self.tab_snapshot.chunks(input_start..input_end, theme), input_chunk: Default::default(), output_position: output_start, max_output_row: rows.end, @@ -1233,7 +1234,7 @@ mod tests { } let actual_text = self - .chunks(start_row..end_row, false) + .chunks(start_row..end_row, None) .map(|c| c.text) .collect::(); assert_eq!( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 966396840875ed362556c12443747c44f02e4e24..6e186ef9af88b5f4ee5b8d2df53fe54592b10b5c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -493,7 +493,7 @@ impl EditorElement { let mut styles = Vec::new(); let mut row = rows.start; let mut line_exceeded_max_len = false; - let chunks = snapshot.chunks(rows.clone()); + let chunks = snapshot.chunks(rows.clone(), Some(&style.syntax)); let newline_chunk = Chunk { text: "\n", @@ -517,10 +517,8 @@ impl EditorElement { } if !line_chunk.is_empty() && !line_exceeded_max_len { - let highlight_style = chunk - .highlight_id - .style(&style.syntax) - .unwrap_or(style.text.clone().into()); + let highlight_style = + chunk.highlight_style.unwrap_or(style.text.clone().into()); // Avoid a lookup if the font properties match the previous ones. let font_id = if highlight_style.font_properties == prev_font_properties { prev_font_id diff --git a/crates/editor/src/lib.rs b/crates/editor/src/lib.rs index ffd9e961bc1cd612a123fdd78a4885663956dace..7d4f05dba68a1cf3b2863174db9ae436568c08be 100644 --- a/crates/editor/src/lib.rs +++ b/crates/editor/src/lib.rs @@ -31,7 +31,7 @@ use std::{ time::Duration, }; use sum_tree::Bias; -use theme::EditorStyle; +use theme::{EditorStyle, SyntaxTheme}; use util::post_inc; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); @@ -2709,8 +2709,12 @@ impl Snapshot { self.display_snapshot.buffer_rows(start_row) } - pub fn chunks(&mut self, display_rows: Range) -> display_map::Chunks { - self.display_snapshot.chunks(display_rows) + pub fn chunks<'a>( + &'a self, + display_rows: Range, + theme: Option<&'a SyntaxTheme>, + ) -> display_map::Chunks<'a> { + self.display_snapshot.chunks(display_rows, theme) } pub fn scroll_position(&self) -> Vector2F { diff --git a/crates/gpui/src/fonts.rs b/crates/gpui/src/fonts.rs index b1aae4c9be323aa54bbd23b4ff4aa463e4a0882e..3dbfd660344faabcb8352b9f8fe22f6c36f779a3 100644 --- a/crates/gpui/src/fonts.rs +++ b/crates/gpui/src/fonts.rs @@ -30,7 +30,7 @@ pub struct TextStyle { pub underline: Option, } -#[derive(Clone, Debug, Default)] +#[derive(Copy, Clone, Debug, Default)] pub struct HighlightStyle { pub color: Color, pub font_properties: Properties, diff --git a/crates/language/src/lib.rs b/crates/language/src/lib.rs index 85b720d7097a46c959a9440e671c46fd1ca1663b..e2747504b93349638ee99f61d29cfa4c1bbdd616 100644 --- a/crates/language/src/lib.rs +++ b/crates/language/src/lib.rs @@ -12,7 +12,7 @@ use anyhow::{anyhow, Result}; pub use buffer::{Buffer as TextBuffer, Operation as _, *}; use clock::ReplicaId; use futures::FutureExt as _; -use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task}; +use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, MutableAppContext, Task}; use lazy_static::lazy_static; use lsp::LanguageServer; use parking_lot::Mutex; @@ -34,6 +34,7 @@ use std::{ time::{Duration, Instant, SystemTime, UNIX_EPOCH}, vec, }; +use theme::SyntaxTheme; use tree_sitter::{InputEdit, Parser, QueryCursor, Tree}; use util::{post_inc, TryFutureExt as _}; @@ -190,6 +191,7 @@ struct Highlights<'a> { next_capture: Option<(tree_sitter::QueryMatch<'a, 'a>, usize)>, stack: Vec<(usize, HighlightId)>, highlight_map: HighlightMap, + theme: &'a SyntaxTheme, _query_cursor: QueryCursorHandle, } @@ -207,7 +209,7 @@ pub struct Chunks<'a> { #[derive(Clone, Copy, Debug, Default)] pub struct Chunk<'a> { pub text: &'a str, - pub highlight_id: HighlightId, + pub highlight_style: Option, pub diagnostic: Option, } @@ -1634,12 +1636,16 @@ impl Snapshot { .all(|chunk| chunk.matches(|c: char| !c.is_whitespace()).next().is_none()) } - pub fn chunks(&self, range: Range, highlight: bool) -> Chunks { + pub fn chunks<'a, T: ToOffset>( + &'a self, + range: Range, + theme: Option<&'a SyntaxTheme>, + ) -> Chunks<'a> { let range = range.start.to_offset(&*self)..range.end.to_offset(&*self); let mut highlights = None; let mut diagnostic_endpoints = Vec::::new(); - if highlight { + if let Some(theme) = theme { for (_, range, diagnostic) in self.diagnostics .intersecting_ranges(range.clone(), self.content(), true) @@ -1676,6 +1682,7 @@ impl Snapshot { stack: Default::default(), highlight_map: language.highlight_map(), _query_cursor: query_cursor, + theme, }) } } @@ -1845,12 +1852,12 @@ impl<'a> Iterator for Chunks<'a> { let mut chunk_end = (self.chunks.offset() + chunk.len()) .min(next_capture_start) .min(next_diagnostic_endpoint); - let mut highlight_id = HighlightId::default(); - if let Some((parent_capture_end, parent_highlight_id)) = - self.highlights.as_ref().and_then(|h| h.stack.last()) - { - chunk_end = chunk_end.min(*parent_capture_end); - highlight_id = *parent_highlight_id; + let mut highlight_style = None; + if let Some(highlights) = self.highlights.as_ref() { + if let Some((parent_capture_end, parent_highlight_id)) = highlights.stack.last() { + chunk_end = chunk_end.min(*parent_capture_end); + highlight_style = parent_highlight_id.style(highlights.theme); + } } let slice = @@ -1862,7 +1869,7 @@ impl<'a> Iterator for Chunks<'a> { Some(Chunk { text: slice, - highlight_id, + highlight_style, diagnostic: self.current_diagnostic_severity(), }) } else { diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 23bb2b8589bacfca1fb22fca9dc0c29169a994d3..c73b29224fa783671a9e4d63130e9e2ed804d515 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -906,7 +906,7 @@ fn chunks_with_diagnostics( range: Range, ) -> Vec<(String, Option)> { let mut chunks: Vec<(String, Option)> = Vec::new(); - for chunk in buffer.snapshot().chunks(range, true) { + for chunk in buffer.snapshot().chunks(range, Some(&Default::default())) { if chunks .last() .map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic)