use super::{ Highlights, fold_map::{self, Chunk, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},}; use language::Point;use multi_buffer::MultiBufferSnapshot;use std::{cmp, num::NonZeroU32, ops::Range};use sum_tree::Bias; const MAX_EXPANSION_COLUMN: u32 = 256; // Handles a tab width <= 128const SPACES: &[u8; rope::Chunk::MASK_BITS] = &[b' '; _];const MAX_TABS: NonZeroU32 = NonZeroU32::new(SPACES.len() as u32).unwrap(); /// Keeps track of hard tabs in a text buffer.////// See the [`display_map` module documentation](crate::display_map) for more information.pub struct TabMap(TabSnapshot); impl TabMap { #[ztracing::instrument(skip_all)] pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) { let snapshot = TabSnapshot { fold_snapshot, tab_size: tab_size.min(MAX_TABS), max_expansion_column: MAX_EXPANSION_COLUMN, version: 0, }; (Self(snapshot.clone()), snapshot) } #[cfg(test)] pub fn set_max_expansion_column(&mut self, column: u32) -> TabSnapshot { self.0.max_expansion_column = column; self.0.clone() } #[ztracing::instrument(skip_all)] pub fn sync( &mut self, fold_snapshot: FoldSnapshot, mut fold_edits: Vec<FoldEdit>, tab_size: NonZeroU32, ) -> (TabSnapshot, Vec<TabEdit>) { let tab_size = tab_size.min(MAX_TABS); if self.0.tab_size != tab_size { let old_max_point = self.0.max_point(); self.0.version += 1; self.0.fold_snapshot = fold_snapshot; self.0.tab_size = tab_size; return ( self.0.clone(), vec![TabEdit { old: TabPoint::zero()..old_max_point, new: TabPoint::zero()..self.0.max_point(), }], ); } let old_snapshot = &mut self.0; let mut new_version = old_snapshot.version; if old_snapshot.fold_snapshot.version != fold_snapshot.version { new_version += 1; } if fold_edits.is_empty() { old_snapshot.version = new_version; old_snapshot.fold_snapshot = fold_snapshot; old_snapshot.tab_size = tab_size; return (old_snapshot.clone(), vec![]); } // Expand each edit to include the next tab on the same line as the edit, // and any subsequent tabs on that line that moved across the tab expansion // boundary. // // This is necessary because a tab's display width depends on its column // position: it expands to fill up to the next tab stop. When an edit // shifts text on a line, any tab character after the edit may now render // at a different width even though the tab byte itself wasn't touched. // Additionally, tabs beyond `max_expansion_column` are rendered as a // single space instead of expanding to the next tab stop. An edit that // shifts a tab across that boundary changes its display width, so the // edit must cover it. We scan forward from the edit end to the end of // the line, extending the edit to include the first subsequent tab (whose // rendered width may have changed) and the last tab that crossed the // expansion boundary (transitioning between expanded and non-expanded). for fold_edit in &mut fold_edits { let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot); let old_end_row_successor_offset = cmp::min( FoldPoint::new(old_end.row() + 1, 0), old_snapshot.fold_snapshot.max_point(), ) .to_offset(&old_snapshot.fold_snapshot); let new_end = fold_edit.new.end.to_point(&fold_snapshot); let mut offset_from_edit = 0; let mut first_tab_offset = None; let mut last_tab_with_changed_expansion_offset = None; 'outer: for chunk in old_snapshot.fold_snapshot.chunks( fold_edit.old.end..old_end_row_successor_offset, false, Highlights::default(), ) { let mut remaining_tabs = chunk.tabs; while remaining_tabs != 0 { let ix = remaining_tabs.trailing_zeros(); let offset_from_edit = offset_from_edit + ix; if first_tab_offset.is_none() { first_tab_offset = Some(offset_from_edit); } let old_column = old_end.column() + offset_from_edit; let new_column = new_end.column() + offset_from_edit; let was_expanded = old_column < old_snapshot.max_expansion_column; let is_expanded = new_column < old_snapshot.max_expansion_column; if was_expanded != is_expanded { last_tab_with_changed_expansion_offset = Some(offset_from_edit); } else if !was_expanded && !is_expanded { break 'outer; } remaining_tabs &= remaining_tabs - 1; } offset_from_edit += chunk.text.len() as u32; if old_end.column() + offset_from_edit >= old_snapshot.max_expansion_column && new_end.column() + offset_from_edit >= old_snapshot.max_expansion_column { break; } } if let Some(offset) = last_tab_with_changed_expansion_offset.or(first_tab_offset) { fold_edit.old.end.0 += offset as usize + 1; fold_edit.new.end.0 += offset as usize + 1; } } let new_snapshot = TabSnapshot { fold_snapshot, tab_size, max_expansion_column: old_snapshot.max_expansion_column, version: new_version, }; let _old_alloc_ptr = fold_edits.as_ptr(); // Combine any edits that overlap due to the expansion. let mut fold_edits = fold_edits.into_iter(); let mut first_edit = fold_edits.next().unwrap(); // This code relies on reusing allocations from the Vec<_> - at the time of writing .flatten() prevents them. #[allow(clippy::filter_map_identity)] let mut v: Vec<_> = fold_edits .scan(&mut first_edit, |state, edit| { if state.old.end >= edit.old.start { state.old.end = edit.old.end; state.new.end = edit.new.end; Some(None) // Skip this edit, it's merged } else { let new_state = edit; let result = Some(Some(state.clone())); // Yield the previous edit **state = new_state; result } }) .filter_map(|x| x) .collect(); v.push(first_edit); debug_assert_eq!(v.as_ptr(), _old_alloc_ptr, "Fold edits were reallocated"); let tab_edits = v .into_iter() .map(|fold_edit| { let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot); let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot); let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot); let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot); TabEdit { old: old_snapshot.fold_point_to_tab_point(old_start) ..old_snapshot.fold_point_to_tab_point(old_end), new: new_snapshot.fold_point_to_tab_point(new_start) ..new_snapshot.fold_point_to_tab_point(new_end), } }) .collect(); *old_snapshot = new_snapshot; (old_snapshot.clone(), tab_edits) }} #[derive(Clone)]pub struct TabSnapshot { pub fold_snapshot: FoldSnapshot, pub tab_size: NonZeroU32, /// The maximum column up to which a tab can expand. /// Any tab after this column will not expand. pub max_expansion_column: u32, pub version: usize,} impl std::ops::Deref for TabSnapshot { type Target = FoldSnapshot; fn deref(&self) -> &Self::Target { &self.fold_snapshot }} impl TabSnapshot { #[ztracing::instrument(skip_all)] pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot { &self.fold_snapshot.inlay_snapshot.buffer } #[ztracing::instrument(skip_all)] pub fn line_len(&self, row: u32) -> u32 { let max_point = self.max_point(); if row < max_point.row() { self.fold_point_to_tab_point(FoldPoint::new(row, self.fold_snapshot.line_len(row))) .0 .column } else { max_point.column() } } #[ztracing::instrument(skip_all)] pub fn text_summary(&self) -> TextSummary { self.text_summary_for_range(TabPoint::zero()..self.max_point()) } #[ztracing::instrument(skip_all, fields(rows))] pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary { let input_start = self.tab_point_to_fold_point(range.start, Bias::Left).0; let input_end = self.tab_point_to_fold_point(range.end, Bias::Right).0; let input_summary = self .fold_snapshot .text_summary_for_range(input_start..input_end); let line_end = if range.start.row() == range.end.row() { range.end } else { self.max_point() }; let first_line_chars = self .chunks(range.start..line_end, false, Highlights::default()) .flat_map(|chunk| chunk.text.chars()) .take_while(|&c| c != '\n') .count() as u32; let last_line_chars = if range.start.row() == range.end.row() { first_line_chars } else { self.chunks( TabPoint::new(range.end.row(), 0)..range.end, false, Highlights::default(), ) .flat_map(|chunk| chunk.text.chars()) .count() as u32 }; TextSummary { lines: range.end.0 - range.start.0, first_line_chars, last_line_chars, longest_row: input_summary.longest_row, longest_row_chars: input_summary.longest_row_chars, } } #[ztracing::instrument(skip_all)] pub(crate) fn chunks<'a>( &'a self, range: Range<TabPoint>, language_aware: bool, highlights: Highlights<'a>, ) -> TabChunks<'a> { let (input_start, expanded_char_column, to_next_stop) = self.tab_point_to_fold_point(range.start, Bias::Left); let input_column = input_start.column(); let input_start = input_start.to_offset(&self.fold_snapshot); let input_end = self .tab_point_to_fold_point(range.end, Bias::Right) .0 .to_offset(&self.fold_snapshot); let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 { range.end.column() - range.start.column() } else { to_next_stop }; TabChunks { snapshot: self, fold_chunks: self.fold_snapshot.chunks( input_start..input_end, language_aware, highlights, ), input_column, column: expanded_char_column, max_expansion_column: self.max_expansion_column, output_position: range.start.0, max_output_position: range.end.0, tab_size: self.tab_size, chunk: Chunk { text: unsafe { std::str::from_utf8_unchecked(&SPACES[..to_next_stop as usize]) }, is_tab: true, chars: 1u128.unbounded_shl(to_next_stop) - 1, ..Default::default() }, inside_leading_tab: to_next_stop > 0, } } #[ztracing::instrument(skip_all)] pub fn rows(&self, row: u32) -> fold_map::FoldRows<'_> { self.fold_snapshot.row_infos(row) } #[cfg(test)] #[ztracing::instrument(skip_all)] pub fn text(&self) -> String { self.chunks( TabPoint::zero()..self.max_point(), false, Highlights::default(), ) .map(|chunk| chunk.text) .collect() } #[ztracing::instrument(skip_all)] pub fn max_point(&self) -> TabPoint { self.fold_point_to_tab_point(self.fold_snapshot.max_point()) } #[ztracing::instrument(skip_all)] pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint { self.fold_point_to_tab_point( self.fold_snapshot .clip_point(self.tab_point_to_fold_point(point, bias).0, bias), ) } #[ztracing::instrument(skip_all)] pub fn fold_point_to_tab_point(&self, input: FoldPoint) -> TabPoint { let chunks = self.fold_snapshot.chunks_at(FoldPoint::new(input.row(), 0)); let tab_cursor = TabStopCursor::new(chunks); let expanded = self.expand_tabs(tab_cursor, input.column()); TabPoint::new(input.row(), expanded) } #[ztracing::instrument(skip_all)] pub fn tab_point_cursor(&self) -> TabPointCursor<'_> { TabPointCursor { this: self } } #[ztracing::instrument(skip_all)] pub fn tab_point_to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) { let chunks = self .fold_snapshot .chunks_at(FoldPoint::new(output.row(), 0)); let tab_cursor = TabStopCursor::new(chunks); let expanded = output.column(); let (collapsed, expanded_char_column, to_next_stop) = self.collapse_tabs(tab_cursor, expanded, bias); ( FoldPoint::new(output.row(), collapsed), expanded_char_column, to_next_stop, ) } #[ztracing::instrument(skip_all)] pub fn point_to_tab_point(&self, point: Point, bias: Bias) -> TabPoint { let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point); let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias); self.fold_point_to_tab_point(fold_point) } #[ztracing::instrument(skip_all)] pub fn tab_point_to_point(&self, point: TabPoint, bias: Bias) -> Point { let fold_point = self.tab_point_to_fold_point(point, bias).0; let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot); self.fold_snapshot .inlay_snapshot .to_buffer_point(inlay_point) } #[ztracing::instrument(skip_all)] fn expand_tabs<'a>(&self, mut cursor: TabStopCursor<'a>, column: u32) -> u32 { // we only ever act on a single row at a time // so the main difference is that other layers build a transform sumtree, and can then just run through that // we cant quite do this here, as we need to work with the previous layer chunk to understand the tabs of the corresponding row // we can still do forward searches for this though, we search for a row, then traverse the column up to where we need to be let tab_size = self.tab_size.get(); let end_column = column.min(self.max_expansion_column); let mut seek_target = end_column; let mut tab_count = 0; let mut expanded_tab_len = 0; while let Some(tab_stop) = cursor.seek_forward(seek_target) { let expanded_chars_old = tab_stop.char_offset + expanded_tab_len - tab_count; let tab_len = tab_size - ((expanded_chars_old - 1) % tab_size); tab_count += 1; expanded_tab_len += tab_len; seek_target = end_column - cursor.byte_offset; } let left_over_char_bytes = if !cursor.is_char_boundary() { cursor.bytes_until_next_char().unwrap_or(0) as u32 } else { 0 }; let collapsed_bytes = cursor.byte_offset() + left_over_char_bytes; let expanded_bytes = cursor.byte_offset() + expanded_tab_len - tab_count + left_over_char_bytes; expanded_bytes + column.saturating_sub(collapsed_bytes) } #[ztracing::instrument(skip_all)] fn collapse_tabs<'a>( &self, mut cursor: TabStopCursor<'a>, column: u32, bias: Bias, ) -> (u32, u32, u32) { let tab_size = self.tab_size.get(); let mut collapsed_column = column; let mut seek_target = column.min(self.max_expansion_column); let mut tab_count = 0; let mut expanded_tab_len = 0; while let Some(tab_stop) = cursor.seek_forward(seek_target) { // Calculate how much we want to expand this tab stop (into spaces) let expanded_chars_old = tab_stop.char_offset + expanded_tab_len - tab_count; let tab_len = tab_size - ((expanded_chars_old - 1) % tab_size); // Increment tab count tab_count += 1; // The count of how many spaces we've added to this line in place of tab bytes expanded_tab_len += tab_len; // The count of bytes at this point in the iteration while considering tab_count and previous expansions let expanded_bytes = tab_stop.byte_offset + expanded_tab_len - tab_count; // Did we expand past the search target? if expanded_bytes > column { let mut expanded_chars = tab_stop.char_offset + expanded_tab_len - tab_count; // We expanded past the search target, so need to account for the offshoot expanded_chars -= expanded_bytes - column; return match bias { Bias::Left => ( cursor.byte_offset() - 1, expanded_chars, expanded_bytes - column, ), Bias::Right => (cursor.byte_offset(), expanded_chars, 0), }; } else { // otherwise we only want to move the cursor collapse column forward collapsed_column = collapsed_column - tab_len + 1; seek_target = (collapsed_column - cursor.byte_offset) .min(self.max_expansion_column - cursor.byte_offset); } } let collapsed_bytes = cursor.byte_offset(); let expanded_bytes = cursor.byte_offset() + expanded_tab_len - tab_count; let expanded_chars = cursor.char_offset() + expanded_tab_len - tab_count; ( collapsed_bytes + column.saturating_sub(expanded_bytes), expanded_chars, 0, ) }} // todo(lw): Implement TabPointCursor properlypub struct TabPointCursor<'this> { this: &'this TabSnapshot,} impl TabPointCursor<'_> { #[ztracing::instrument(skip_all)] pub fn map(&mut self, point: FoldPoint) -> TabPoint { self.this.fold_point_to_tab_point(point) }} #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]pub struct TabPoint(pub Point); Lines 1–500 of 1699. Load more
use super::{ Highlights, fold_map::{self, Chunk, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot}, }; use language::Point; use multi_buffer::MultiBufferSnapshot; use std::{cmp, num::NonZeroU32, ops::Range}; use sum_tree::Bias; const MAX_EXPANSION_COLUMN: u32 = 256; // Handles a tab width <= 128 const SPACES: &[u8; rope::Chunk::MASK_BITS] = &[b' '; _]; const MAX_TABS: NonZeroU32 = NonZeroU32::new(SPACES.len() as u32).unwrap(); /// Keeps track of hard tabs in a text buffer. /// /// See the [`display_map` module documentation](crate::display_map) for more information. pub struct TabMap(TabSnapshot); impl TabMap { #[ztracing::instrument(skip_all)] pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) { let snapshot = TabSnapshot { fold_snapshot, tab_size: tab_size.min(MAX_TABS), max_expansion_column: MAX_EXPANSION_COLUMN, version: 0, }; (Self(snapshot.clone()), snapshot) } #[cfg(test)] pub fn set_max_expansion_column(&mut self, column: u32) -> TabSnapshot { self.0.max_expansion_column = column; self.0.clone() } #[ztracing::instrument(skip_all)] pub fn sync( &mut self, fold_snapshot: FoldSnapshot, mut fold_edits: Vec<FoldEdit>, tab_size: NonZeroU32, ) -> (TabSnapshot, Vec<TabEdit>) { let tab_size = tab_size.min(MAX_TABS); if self.0.tab_size != tab_size { let old_max_point = self.0.max_point(); self.0.version += 1; self.0.fold_snapshot = fold_snapshot; self.0.tab_size = tab_size; return ( self.0.clone(), vec![TabEdit { old: TabPoint::zero()..old_max_point, new: TabPoint::zero()..self.0.max_point(), }], ); } let old_snapshot = &mut self.0; let mut new_version = old_snapshot.version; if old_snapshot.fold_snapshot.version != fold_snapshot.version { new_version += 1; } if fold_edits.is_empty() { old_snapshot.version = new_version; old_snapshot.fold_snapshot = fold_snapshot; old_snapshot.tab_size = tab_size; return (old_snapshot.clone(), vec![]); } // Expand each edit to include the next tab on the same line as the edit, // and any subsequent tabs on that line that moved across the tab expansion // boundary. // // This is necessary because a tab's display width depends on its column // position: it expands to fill up to the next tab stop. When an edit // shifts text on a line, any tab character after the edit may now render // at a different width even though the tab byte itself wasn't touched. // Additionally, tabs beyond `max_expansion_column` are rendered as a // single space instead of expanding to the next tab stop. An edit that // shifts a tab across that boundary changes its display width, so the // edit must cover it. We scan forward from the edit end to the end of // the line, extending the edit to include the first subsequent tab (whose // rendered width may have changed) and the last tab that crossed the // expansion boundary (transitioning between expanded and non-expanded). for fold_edit in &mut fold_edits { let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot); let old_end_row_successor_offset = cmp::min( FoldPoint::new(old_end.row() + 1, 0), old_snapshot.fold_snapshot.max_point(), ) .to_offset(&old_snapshot.fold_snapshot); let new_end = fold_edit.new.end.to_point(&fold_snapshot); let mut offset_from_edit = 0; let mut first_tab_offset = None; let mut last_tab_with_changed_expansion_offset = None; 'outer: for chunk in old_snapshot.fold_snapshot.chunks( fold_edit.old.end..old_end_row_successor_offset, false, Highlights::default(), ) { let mut remaining_tabs = chunk.tabs; while remaining_tabs != 0 { let ix = remaining_tabs.trailing_zeros(); let offset_from_edit = offset_from_edit + ix; if first_tab_offset.is_none() { first_tab_offset = Some(offset_from_edit); } let old_column = old_end.column() + offset_from_edit; let new_column = new_end.column() + offset_from_edit; let was_expanded = old_column < old_snapshot.max_expansion_column; let is_expanded = new_column < old_snapshot.max_expansion_column; if was_expanded != is_expanded { last_tab_with_changed_expansion_offset = Some(offset_from_edit); } else if !was_expanded && !is_expanded { break 'outer; } remaining_tabs &= remaining_tabs - 1; } offset_from_edit += chunk.text.len() as u32; if old_end.column() + offset_from_edit >= old_snapshot.max_expansion_column && new_end.column() + offset_from_edit >= old_snapshot.max_expansion_column { break; } } if let Some(offset) = last_tab_with_changed_expansion_offset.or(first_tab_offset) { fold_edit.old.end.0 += offset as usize + 1; fold_edit.new.end.0 += offset as usize + 1; } } let new_snapshot = TabSnapshot { fold_snapshot, tab_size, max_expansion_column: old_snapshot.max_expansion_column, version: new_version, }; let _old_alloc_ptr = fold_edits.as_ptr(); // Combine any edits that overlap due to the expansion. let mut fold_edits = fold_edits.into_iter(); let mut first_edit = fold_edits.next().unwrap(); // This code relies on reusing allocations from the Vec<_> - at the time of writing .flatten() prevents them. #[allow(clippy::filter_map_identity)] let mut v: Vec<_> = fold_edits .scan(&mut first_edit, |state, edit| { if state.old.end >= edit.old.start { state.old.end = edit.old.end; state.new.end = edit.new.end; Some(None) // Skip this edit, it's merged } else { let new_state = edit; let result = Some(Some(state.clone())); // Yield the previous edit **state = new_state; result } }) .filter_map(|x| x) .collect(); v.push(first_edit); debug_assert_eq!(v.as_ptr(), _old_alloc_ptr, "Fold edits were reallocated"); let tab_edits = v .into_iter() .map(|fold_edit| { let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot); let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot); let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot); let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot); TabEdit { old: old_snapshot.fold_point_to_tab_point(old_start) ..old_snapshot.fold_point_to_tab_point(old_end), new: new_snapshot.fold_point_to_tab_point(new_start) ..new_snapshot.fold_point_to_tab_point(new_end), } }) .collect(); *old_snapshot = new_snapshot; (old_snapshot.clone(), tab_edits) } } #[derive(Clone)] pub struct TabSnapshot { pub fold_snapshot: FoldSnapshot, pub tab_size: NonZeroU32, /// The maximum column up to which a tab can expand. /// Any tab after this column will not expand. pub max_expansion_column: u32, pub version: usize, } impl std::ops::Deref for TabSnapshot { type Target = FoldSnapshot; fn deref(&self) -> &Self::Target { &self.fold_snapshot } } impl TabSnapshot { #[ztracing::instrument(skip_all)] pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot { &self.fold_snapshot.inlay_snapshot.buffer } #[ztracing::instrument(skip_all)] pub fn line_len(&self, row: u32) -> u32 { let max_point = self.max_point(); if row < max_point.row() { self.fold_point_to_tab_point(FoldPoint::new(row, self.fold_snapshot.line_len(row))) .0 .column } else { max_point.column() } } #[ztracing::instrument(skip_all)] pub fn text_summary(&self) -> TextSummary { self.text_summary_for_range(TabPoint::zero()..self.max_point()) } #[ztracing::instrument(skip_all, fields(rows))] pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary { let input_start = self.tab_point_to_fold_point(range.start, Bias::Left).0; let input_end = self.tab_point_to_fold_point(range.end, Bias::Right).0; let input_summary = self .fold_snapshot .text_summary_for_range(input_start..input_end); let line_end = if range.start.row() == range.end.row() { range.end } else { self.max_point() }; let first_line_chars = self .chunks(range.start..line_end, false, Highlights::default()) .flat_map(|chunk| chunk.text.chars()) .take_while(|&c| c != '\n') .count() as u32; let last_line_chars = if range.start.row() == range.end.row() { first_line_chars } else { self.chunks( TabPoint::new(range.end.row(), 0)..range.end, false, Highlights::default(), ) .flat_map(|chunk| chunk.text.chars()) .count() as u32 }; TextSummary { lines: range.end.0 - range.start.0, first_line_chars, last_line_chars, longest_row: input_summary.longest_row, longest_row_chars: input_summary.longest_row_chars, } } #[ztracing::instrument(skip_all)] pub(crate) fn chunks<'a>( &'a self, range: Range<TabPoint>, language_aware: bool, highlights: Highlights<'a>, ) -> TabChunks<'a> { let (input_start, expanded_char_column, to_next_stop) = self.tab_point_to_fold_point(range.start, Bias::Left); let input_column = input_start.column(); let input_start = input_start.to_offset(&self.fold_snapshot); let input_end = self .tab_point_to_fold_point(range.end, Bias::Right) .0 .to_offset(&self.fold_snapshot); let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 { range.end.column() - range.start.column() } else { to_next_stop }; TabChunks { snapshot: self, fold_chunks: self.fold_snapshot.chunks( input_start..input_end, language_aware, highlights, ), input_column, column: expanded_char_column, max_expansion_column: self.max_expansion_column, output_position: range.start.0, max_output_position: range.end.0, tab_size: self.tab_size, chunk: Chunk { text: unsafe { std::str::from_utf8_unchecked(&SPACES[..to_next_stop as usize]) }, is_tab: true, chars: 1u128.unbounded_shl(to_next_stop) - 1, ..Default::default() }, inside_leading_tab: to_next_stop > 0, } } #[ztracing::instrument(skip_all)] pub fn rows(&self, row: u32) -> fold_map::FoldRows<'_> { self.fold_snapshot.row_infos(row) } #[cfg(test)] #[ztracing::instrument(skip_all)] pub fn text(&self) -> String { self.chunks( TabPoint::zero()..self.max_point(), false, Highlights::default(), ) .map(|chunk| chunk.text) .collect() } #[ztracing::instrument(skip_all)] pub fn max_point(&self) -> TabPoint { self.fold_point_to_tab_point(self.fold_snapshot.max_point()) } #[ztracing::instrument(skip_all)] pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint { self.fold_point_to_tab_point( self.fold_snapshot .clip_point(self.tab_point_to_fold_point(point, bias).0, bias), ) } #[ztracing::instrument(skip_all)] pub fn fold_point_to_tab_point(&self, input: FoldPoint) -> TabPoint { let chunks = self.fold_snapshot.chunks_at(FoldPoint::new(input.row(), 0)); let tab_cursor = TabStopCursor::new(chunks); let expanded = self.expand_tabs(tab_cursor, input.column()); TabPoint::new(input.row(), expanded) } #[ztracing::instrument(skip_all)] pub fn tab_point_cursor(&self) -> TabPointCursor<'_> { TabPointCursor { this: self } } #[ztracing::instrument(skip_all)] pub fn tab_point_to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) { let chunks = self .fold_snapshot .chunks_at(FoldPoint::new(output.row(), 0)); let tab_cursor = TabStopCursor::new(chunks); let expanded = output.column(); let (collapsed, expanded_char_column, to_next_stop) = self.collapse_tabs(tab_cursor, expanded, bias); ( FoldPoint::new(output.row(), collapsed), expanded_char_column, to_next_stop, ) } #[ztracing::instrument(skip_all)] pub fn point_to_tab_point(&self, point: Point, bias: Bias) -> TabPoint { let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point); let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias); self.fold_point_to_tab_point(fold_point) } #[ztracing::instrument(skip_all)] pub fn tab_point_to_point(&self, point: TabPoint, bias: Bias) -> Point { let fold_point = self.tab_point_to_fold_point(point, bias).0; let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot); self.fold_snapshot .inlay_snapshot .to_buffer_point(inlay_point) } #[ztracing::instrument(skip_all)] fn expand_tabs<'a>(&self, mut cursor: TabStopCursor<'a>, column: u32) -> u32 { // we only ever act on a single row at a time // so the main difference is that other layers build a transform sumtree, and can then just run through that // we cant quite do this here, as we need to work with the previous layer chunk to understand the tabs of the corresponding row // we can still do forward searches for this though, we search for a row, then traverse the column up to where we need to be let tab_size = self.tab_size.get(); let end_column = column.min(self.max_expansion_column); let mut seek_target = end_column; let mut tab_count = 0; let mut expanded_tab_len = 0; while let Some(tab_stop) = cursor.seek_forward(seek_target) { let expanded_chars_old = tab_stop.char_offset + expanded_tab_len - tab_count; let tab_len = tab_size - ((expanded_chars_old - 1) % tab_size); tab_count += 1; expanded_tab_len += tab_len; seek_target = end_column - cursor.byte_offset; } let left_over_char_bytes = if !cursor.is_char_boundary() { cursor.bytes_until_next_char().unwrap_or(0) as u32 } else { 0 }; let collapsed_bytes = cursor.byte_offset() + left_over_char_bytes; let expanded_bytes = cursor.byte_offset() + expanded_tab_len - tab_count + left_over_char_bytes; expanded_bytes + column.saturating_sub(collapsed_bytes) } #[ztracing::instrument(skip_all)] fn collapse_tabs<'a>( &self, mut cursor: TabStopCursor<'a>, column: u32, bias: Bias, ) -> (u32, u32, u32) { let tab_size = self.tab_size.get(); let mut collapsed_column = column; let mut seek_target = column.min(self.max_expansion_column); let mut tab_count = 0; let mut expanded_tab_len = 0; while let Some(tab_stop) = cursor.seek_forward(seek_target) { // Calculate how much we want to expand this tab stop (into spaces) let expanded_chars_old = tab_stop.char_offset + expanded_tab_len - tab_count; let tab_len = tab_size - ((expanded_chars_old - 1) % tab_size); // Increment tab count tab_count += 1; // The count of how many spaces we've added to this line in place of tab bytes expanded_tab_len += tab_len; // The count of bytes at this point in the iteration while considering tab_count and previous expansions let expanded_bytes = tab_stop.byte_offset + expanded_tab_len - tab_count; // Did we expand past the search target? if expanded_bytes > column { let mut expanded_chars = tab_stop.char_offset + expanded_tab_len - tab_count; // We expanded past the search target, so need to account for the offshoot expanded_chars -= expanded_bytes - column; return match bias { Bias::Left => ( cursor.byte_offset() - 1, expanded_chars, expanded_bytes - column, ), Bias::Right => (cursor.byte_offset(), expanded_chars, 0), }; } else { // otherwise we only want to move the cursor collapse column forward collapsed_column = collapsed_column - tab_len + 1; seek_target = (collapsed_column - cursor.byte_offset) .min(self.max_expansion_column - cursor.byte_offset); } } let collapsed_bytes = cursor.byte_offset(); let expanded_bytes = cursor.byte_offset() + expanded_tab_len - tab_count; let expanded_chars = cursor.char_offset() + expanded_tab_len - tab_count; ( collapsed_bytes + column.saturating_sub(expanded_bytes), expanded_chars, 0, ) } } // todo(lw): Implement TabPointCursor properly pub struct TabPointCursor<'this> { this: &'this TabSnapshot, } impl TabPointCursor<'_> { #[ztracing::instrument(skip_all)] pub fn map(&mut self, point: FoldPoint) -> TabPoint { self.this.fold_point_to_tab_point(point) } } #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] pub struct TabPoint(pub Point);