diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index cdf4b7e3ad..cdad986b49 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -648,7 +648,7 @@ impl DisplaySnapshot { false } }), - buffer.line_len(buffer_row) as usize, // Never collapse + buffer.line_len(buffer_row), // Never collapse ); (indent_size as u32, is_blank) diff --git a/crates/editor/src/display_map/suggestion_map.rs b/crates/editor/src/display_map/suggestion_map.rs index 2d0225644f..5be4f6f0df 100644 --- a/crates/editor/src/display_map/suggestion_map.rs +++ b/crates/editor/src/display_map/suggestion_map.rs @@ -210,6 +210,32 @@ impl SuggestionSnapshot { } } + pub fn line_len(&self, row: u32) -> u32 { + if let Some(suggestion) = &self.suggestion { + let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0; + let suggestion_end = suggestion_start + suggestion.text.max_point(); + + if row < suggestion_start.row { + self.fold_snapshot.line_len(row) + } else if row > suggestion_end.row { + self.fold_snapshot + .line_len(suggestion_start.row + (row - suggestion_end.row)) + } else { + let mut result = suggestion.text.line_len(row - suggestion_start.row); + if row == suggestion_start.row { + result += suggestion_start.column; + } + if row == suggestion_end.row { + result += + self.fold_snapshot.line_len(suggestion_start.row) - suggestion_start.column; + } + result + } + } else { + self.fold_snapshot.line_len(row) + } + } + pub fn clip_point(&self, point: SuggestionPoint, bias: Bias) -> SuggestionPoint { if let Some(suggestion) = self.suggestion.as_ref() { let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0; diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index 29dca3b1fb..55b3425b24 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -8,6 +8,8 @@ use parking_lot::Mutex; use std::{cmp, mem, num::NonZeroU32, ops::Range}; use sum_tree::Bias; +const MAX_EXPANSION_COLUMN: u32 = 256; + pub struct TabMap(Mutex); impl TabMap { @@ -15,11 +17,18 @@ impl TabMap { let snapshot = TabSnapshot { suggestion_snapshot: input, tab_size, + max_expansion_column: MAX_EXPANSION_COLUMN, version: 0, }; (Self(Mutex::new(snapshot.clone())), snapshot) } + #[cfg(test)] + pub fn set_max_expansion_column(&self, column: u32) -> TabSnapshot { + self.0.lock().max_expansion_column = column; + self.0.lock().clone() + } + pub fn sync( &self, suggestion_snapshot: SuggestionSnapshot, @@ -30,6 +39,7 @@ impl TabMap { let mut new_snapshot = TabSnapshot { suggestion_snapshot, tab_size, + max_expansion_column: old_snapshot.max_expansion_column, version: old_snapshot.version, }; @@ -111,6 +121,7 @@ impl TabMap { pub struct TabSnapshot { pub suggestion_snapshot: SuggestionSnapshot, pub tab_size: NonZeroU32, + pub max_expansion_column: u32, pub version: usize, } @@ -122,14 +133,12 @@ impl TabSnapshot { pub fn line_len(&self, row: u32) -> u32 { let max_point = self.max_point(); if row < max_point.row() { - self.chunks( - TabPoint::new(row, 0)..TabPoint::new(row + 1, 0), - false, - None, - ) - .map(|chunk| chunk.text.len() as u32) - .sum::() - - 1 + self.to_tab_point(SuggestionPoint::new( + row, + self.suggestion_snapshot.line_len(row), + )) + .0 + .column } else { max_point.column() } @@ -191,12 +200,13 @@ impl TabSnapshot { ) -> TabChunks<'a> { let (input_start, expanded_char_column, to_next_stop) = self.to_suggestion_point(range.start, Bias::Left); + let input_column = input_start.column(); let input_start = self.suggestion_snapshot.to_offset(input_start); let input_end = self .suggestion_snapshot .to_offset(self.to_suggestion_point(range.end, Bias::Right).0); - let to_next_stop = if range.start.0 + Point::new(0, to_next_stop as u32) > range.end.0 { - (range.end.column() - range.start.column()) as usize + 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 }; @@ -207,12 +217,14 @@ impl TabSnapshot { language_aware, text_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: &SPACES[0..to_next_stop], + text: &SPACES[0..(to_next_stop as usize)], ..Default::default() }, skip_leading_tab: to_next_stop > 0, @@ -245,19 +257,15 @@ impl TabSnapshot { let chars = self .suggestion_snapshot .chars_at(SuggestionPoint::new(input.row(), 0)); - let expanded = self.expand_tabs(chars, input.column() as usize); - TabPoint::new(input.row(), expanded as u32) + let expanded = self.expand_tabs(chars, input.column()); + TabPoint::new(input.row(), expanded) } - pub fn to_suggestion_point( - &self, - output: TabPoint, - bias: Bias, - ) -> (SuggestionPoint, usize, usize) { + pub fn to_suggestion_point(&self, output: TabPoint, bias: Bias) -> (SuggestionPoint, u32, u32) { let chars = self .suggestion_snapshot .chars_at(SuggestionPoint::new(output.row(), 0)); - let expanded = output.column() as usize; + let expanded = output.column(); let (collapsed, expanded_char_column, to_next_stop) = self.collapse_tabs(chars, expanded, bias); ( @@ -282,14 +290,15 @@ impl TabSnapshot { fold_point.to_buffer_point(&self.suggestion_snapshot.fold_snapshot) } - pub fn expand_tabs(&self, chars: impl Iterator, column: usize) -> usize { - let tab_size = self.tab_size.get() as usize; + pub fn expand_tabs(&self, chars: impl Iterator, column: u32) -> u32 { + let tab_size = self.tab_size.get(); let mut expanded_chars = 0; let mut expanded_bytes = 0; let mut collapsed_bytes = 0; + let end_column = column.min(self.max_expansion_column); for c in chars { - if collapsed_bytes == column { + if collapsed_bytes >= end_column { break; } if c == '\t' { @@ -297,21 +306,21 @@ impl TabSnapshot { expanded_bytes += tab_len; expanded_chars += tab_len; } else { - expanded_bytes += c.len_utf8(); + expanded_bytes += c.len_utf8() as u32; expanded_chars += 1; } - collapsed_bytes += c.len_utf8(); + collapsed_bytes += c.len_utf8() as u32; } - expanded_bytes + expanded_bytes + column.saturating_sub(collapsed_bytes) } fn collapse_tabs( &self, chars: impl Iterator, - column: usize, + column: u32, bias: Bias, - ) -> (usize, usize, usize) { - let tab_size = self.tab_size.get() as usize; + ) -> (u32, u32, u32) { + let tab_size = self.tab_size.get(); let mut expanded_bytes = 0; let mut expanded_chars = 0; @@ -320,6 +329,9 @@ impl TabSnapshot { if expanded_bytes >= column { break; } + if collapsed_bytes >= self.max_expansion_column { + break; + } if c == '\t' { let tab_len = tab_size - (expanded_chars % tab_size); @@ -334,7 +346,7 @@ impl TabSnapshot { } } else { expanded_chars += 1; - expanded_bytes += c.len_utf8(); + expanded_bytes += c.len_utf8() as u32; } if expanded_bytes > column && matches!(bias, Bias::Left) { @@ -342,9 +354,13 @@ impl TabSnapshot { break; } - collapsed_bytes += c.len_utf8(); + collapsed_bytes += c.len_utf8() as u32; } - (collapsed_bytes, expanded_chars, 0) + ( + collapsed_bytes + column.saturating_sub(expanded_bytes), + expanded_chars, + 0, + ) } } @@ -432,8 +448,10 @@ const SPACES: &str = " "; pub struct TabChunks<'a> { suggestion_chunks: SuggestionChunks<'a>, chunk: Chunk<'a>, - column: usize, + column: u32, + max_expansion_column: u32, output_position: Point, + input_column: u32, max_output_position: Point, tab_size: NonZeroU32, skip_leading_tab: bool, @@ -467,14 +485,19 @@ impl<'a> Iterator for TabChunks<'a> { }); } else { self.chunk.text = &self.chunk.text[1..]; - let tab_size = self.tab_size.get() as u32; - let mut len = tab_size - self.column as u32 % tab_size; + let tab_size = if self.input_column < self.max_expansion_column { + self.tab_size.get() as u32 + } else { + 1 + }; + let mut len = tab_size - self.column % tab_size; let next_output_position = cmp::min( self.output_position + Point::new(0, len), self.max_output_position, ); len = next_output_position.column - self.output_position.column; - self.column += len as usize; + self.column += len; + self.input_column += 1; self.output_position = next_output_position; return Some(Chunk { text: &SPACES[0..len as usize], @@ -484,10 +507,12 @@ impl<'a> Iterator for TabChunks<'a> { } '\n' => { self.column = 0; + self.input_column = 0; self.output_position += Point::new(1, 0); } _ => { self.column += 1; + self.input_column += c.len_utf8() as u32; self.output_position.column += c.len_utf8() as u32; } } @@ -512,11 +537,76 @@ mod tests { let buffer_snapshot = buffer.read(cx).snapshot(cx); let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone()); let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot); - let (_, tabs_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap()); + let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap()); - assert_eq!(tabs_snapshot.expand_tabs("\t".chars(), 0), 0); - assert_eq!(tabs_snapshot.expand_tabs("\t".chars(), 1), 4); - assert_eq!(tabs_snapshot.expand_tabs("\ta".chars(), 2), 5); + assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0); + assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4); + assert_eq!(tab_snapshot.expand_tabs("\ta".chars(), 2), 5); + } + + #[gpui::test] + fn test_long_lines(cx: &mut gpui::MutableAppContext) { + let max_expansion_column = 12; + let input = "A\tBC\tDEF\tG\tHI\tJ\tK\tL\tM"; + let output = "A BC DEF G HI J K L M"; + + let buffer = MultiBuffer::build_simple(input, cx); + let buffer_snapshot = buffer.read(cx).snapshot(cx); + let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone()); + let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot); + let (_, mut tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap()); + + tab_snapshot.max_expansion_column = max_expansion_column; + assert_eq!(tab_snapshot.text(), output); + + for (ix, c) in input.char_indices() { + assert_eq!( + tab_snapshot + .chunks( + TabPoint::new(0, ix as u32)..tab_snapshot.max_point(), + false, + None + ) + .map(|c| c.text) + .collect::(), + &output[ix..], + "text from index {ix}" + ); + + if c != '\t' { + let input_point = Point::new(0, ix as u32); + let output_point = Point::new(0, output.find(c).unwrap() as u32); + assert_eq!( + tab_snapshot.to_tab_point(SuggestionPoint(input_point)), + TabPoint(output_point), + "to_tab_point({input_point:?})" + ); + assert_eq!( + tab_snapshot + .to_suggestion_point(TabPoint(output_point), Bias::Left) + .0, + SuggestionPoint(input_point), + "to_suggestion_point({output_point:?})" + ); + } + } + } + + #[gpui::test] + fn test_long_lines_with_character_spanning_max_expansion_column( + cx: &mut gpui::MutableAppContext, + ) { + let max_expansion_column = 8; + let input = "abcdefg⋯hij"; + + let buffer = MultiBuffer::build_simple(input, cx); + let buffer_snapshot = buffer.read(cx).snapshot(cx); + let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone()); + let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot); + let (_, mut tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap()); + + tab_snapshot.max_expansion_column = max_expansion_column; + assert_eq!(tab_snapshot.text(), input); } #[gpui::test(iterations = 100)] @@ -542,7 +632,9 @@ mod tests { let (suggestion_snapshot, _) = suggestion_map.randomly_mutate(&mut rng); log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text()); - let (_, tabs_snapshot) = TabMap::new(suggestion_snapshot.clone(), tab_size); + let (tab_map, _) = TabMap::new(suggestion_snapshot.clone(), tab_size); + let tabs_snapshot = tab_map.set_max_expansion_column(32); + let text = text::Rope::from(tabs_snapshot.text().as_str()); log::info!( "TabMap text (tab size: {}): {:?}", @@ -586,7 +678,11 @@ mod tests { } for row in 0..=text.max_point().row { - assert_eq!(tabs_snapshot.line_len(row), text.line_len(row)); + assert_eq!( + tabs_snapshot.line_len(row), + text.line_len(row), + "line_len({row})" + ); } } } diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index f0d10ad423..311233850d 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1089,7 +1089,8 @@ mod tests { log::info!("FoldMap text: {:?}", fold_snapshot.text()); let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone()); log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text()); - let (tab_map, tabs_snapshot) = TabMap::new(suggestion_snapshot.clone(), tab_size); + let (tab_map, _) = TabMap::new(suggestion_snapshot.clone(), tab_size); + let tabs_snapshot = tab_map.set_max_expansion_column(32); log::info!("TabMap text: {:?}", tabs_snapshot.text()); let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system);