1use super::{
2 suggestion_map::{self, SuggestionChunks, SuggestionEdit, SuggestionPoint, SuggestionSnapshot},
3 TextHighlights,
4};
5use crate::MultiBufferSnapshot;
6use language::{Chunk, Point};
7use parking_lot::Mutex;
8use std::{cmp, mem, num::NonZeroU32, ops::Range};
9use sum_tree::Bias;
10
11pub struct TabMap(Mutex<TabSnapshot>);
12
13impl TabMap {
14 pub fn new(input: SuggestionSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
15 let snapshot = TabSnapshot {
16 suggestion_snapshot: input,
17 tab_size,
18 version: 0,
19 };
20 (Self(Mutex::new(snapshot.clone())), snapshot)
21 }
22
23 pub fn sync(
24 &self,
25 suggestion_snapshot: SuggestionSnapshot,
26 mut suggestion_edits: Vec<SuggestionEdit>,
27 tab_size: NonZeroU32,
28 ) -> (TabSnapshot, Vec<TabEdit>) {
29 let mut old_snapshot = self.0.lock();
30 let mut new_snapshot = TabSnapshot {
31 suggestion_snapshot,
32 tab_size,
33 version: old_snapshot.version,
34 };
35
36 if old_snapshot.suggestion_snapshot.version != new_snapshot.suggestion_snapshot.version {
37 new_snapshot.version += 1;
38 }
39
40 let old_max_offset = old_snapshot.suggestion_snapshot.len();
41 let mut tab_edits = Vec::with_capacity(suggestion_edits.len());
42
43 if old_snapshot.tab_size == new_snapshot.tab_size {
44 for suggestion_edit in &mut suggestion_edits {
45 let mut delta = 0;
46 for chunk in old_snapshot.suggestion_snapshot.chunks(
47 suggestion_edit.old.end..old_max_offset,
48 false,
49 None,
50 ) {
51 let patterns: &[_] = &['\t', '\n'];
52 if let Some(ix) = chunk.text.find(patterns) {
53 if &chunk.text[ix..ix + 1] == "\t" {
54 suggestion_edit.old.end.0 += delta + ix + 1;
55 suggestion_edit.new.end.0 += delta + ix + 1;
56 }
57
58 break;
59 }
60
61 delta += chunk.text.len();
62 }
63 }
64
65 let mut ix = 1;
66 while ix < suggestion_edits.len() {
67 let (prev_edits, next_edits) = suggestion_edits.split_at_mut(ix);
68 let prev_edit = prev_edits.last_mut().unwrap();
69 let edit = &next_edits[0];
70 if prev_edit.old.end >= edit.old.start {
71 prev_edit.old.end = edit.old.end;
72 prev_edit.new.end = edit.new.end;
73 suggestion_edits.remove(ix);
74 } else {
75 ix += 1;
76 }
77 }
78
79 for suggestion_edit in suggestion_edits {
80 let old_start = old_snapshot
81 .suggestion_snapshot
82 .to_point(suggestion_edit.old.start);
83 let old_end = old_snapshot
84 .suggestion_snapshot
85 .to_point(suggestion_edit.old.end);
86 let new_start = new_snapshot
87 .suggestion_snapshot
88 .to_point(suggestion_edit.new.start);
89 let new_end = new_snapshot
90 .suggestion_snapshot
91 .to_point(suggestion_edit.new.end);
92 tab_edits.push(TabEdit {
93 old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
94 new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
95 });
96 }
97 } else {
98 new_snapshot.version += 1;
99 tab_edits.push(TabEdit {
100 old: TabPoint::zero()..old_snapshot.max_point(),
101 new: TabPoint::zero()..new_snapshot.max_point(),
102 });
103 }
104
105 *old_snapshot = new_snapshot;
106 (old_snapshot.clone(), tab_edits)
107 }
108}
109
110#[derive(Clone)]
111pub struct TabSnapshot {
112 pub suggestion_snapshot: SuggestionSnapshot,
113 pub tab_size: NonZeroU32,
114 pub version: usize,
115}
116
117impl TabSnapshot {
118 pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
119 self.suggestion_snapshot.buffer_snapshot()
120 }
121
122 pub fn line_len(&self, row: u32) -> u32 {
123 let max_point = self.max_point();
124 if row < max_point.row() {
125 self.chunks(
126 TabPoint::new(row, 0)..TabPoint::new(row + 1, 0),
127 false,
128 None,
129 )
130 .map(|chunk| chunk.text.len() as u32)
131 .sum::<u32>()
132 - 1
133 } else {
134 max_point.column()
135 }
136 }
137
138 pub fn text_summary(&self) -> TextSummary {
139 self.text_summary_for_range(TabPoint::zero()..self.max_point())
140 }
141
142 pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary {
143 let input_start = self.to_suggestion_point(range.start, Bias::Left).0;
144 let input_end = self.to_suggestion_point(range.end, Bias::Right).0;
145 let input_summary = self
146 .suggestion_snapshot
147 .text_summary_for_range(input_start..input_end);
148
149 let mut first_line_chars = 0;
150 let line_end = if range.start.row() == range.end.row() {
151 range.end
152 } else {
153 self.max_point()
154 };
155 for c in self
156 .chunks(range.start..line_end, false, None)
157 .flat_map(|chunk| chunk.text.chars())
158 {
159 if c == '\n' {
160 break;
161 }
162 first_line_chars += 1;
163 }
164
165 let mut last_line_chars = 0;
166 if range.start.row() == range.end.row() {
167 last_line_chars = first_line_chars;
168 } else {
169 for _ in self
170 .chunks(TabPoint::new(range.end.row(), 0)..range.end, false, None)
171 .flat_map(|chunk| chunk.text.chars())
172 {
173 last_line_chars += 1;
174 }
175 }
176
177 TextSummary {
178 lines: range.end.0 - range.start.0,
179 first_line_chars,
180 last_line_chars,
181 longest_row: input_summary.longest_row,
182 longest_row_chars: input_summary.longest_row_chars,
183 }
184 }
185
186 pub fn chunks<'a>(
187 &'a self,
188 range: Range<TabPoint>,
189 language_aware: bool,
190 text_highlights: Option<&'a TextHighlights>,
191 ) -> TabChunks<'a> {
192 let (input_start, expanded_char_column, to_next_stop) =
193 self.to_suggestion_point(range.start, Bias::Left);
194 let input_start = self.suggestion_snapshot.to_offset(input_start);
195 let input_end = self
196 .suggestion_snapshot
197 .to_offset(self.to_suggestion_point(range.end, Bias::Right).0);
198 let to_next_stop = if range.start.0 + Point::new(0, to_next_stop as u32) > range.end.0 {
199 (range.end.column() - range.start.column()) as usize
200 } else {
201 to_next_stop
202 };
203
204 TabChunks {
205 suggestion_chunks: self.suggestion_snapshot.chunks(
206 input_start..input_end,
207 language_aware,
208 text_highlights,
209 ),
210 column: expanded_char_column,
211 output_position: range.start.0,
212 max_output_position: range.end.0,
213 tab_size: self.tab_size,
214 chunk: Chunk {
215 text: &SPACES[0..to_next_stop],
216 ..Default::default()
217 },
218 skip_leading_tab: to_next_stop > 0,
219 }
220 }
221
222 pub fn buffer_rows(&self, row: u32) -> suggestion_map::SuggestionBufferRows {
223 self.suggestion_snapshot.buffer_rows(row)
224 }
225
226 #[cfg(test)]
227 pub fn text(&self) -> String {
228 self.chunks(TabPoint::zero()..self.max_point(), false, None)
229 .map(|chunk| chunk.text)
230 .collect()
231 }
232
233 pub fn max_point(&self) -> TabPoint {
234 self.to_tab_point(self.suggestion_snapshot.max_point())
235 }
236
237 pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint {
238 self.to_tab_point(
239 self.suggestion_snapshot
240 .clip_point(self.to_suggestion_point(point, bias).0, bias),
241 )
242 }
243
244 pub fn to_tab_point(&self, input: SuggestionPoint) -> TabPoint {
245 let chars = self
246 .suggestion_snapshot
247 .chars_at(SuggestionPoint::new(input.row(), 0));
248 let expanded = self.expand_tabs(chars, input.column() as usize);
249 TabPoint::new(input.row(), expanded as u32)
250 }
251
252 pub fn to_suggestion_point(
253 &self,
254 output: TabPoint,
255 bias: Bias,
256 ) -> (SuggestionPoint, usize, usize) {
257 let chars = self
258 .suggestion_snapshot
259 .chars_at(SuggestionPoint::new(output.row(), 0));
260 let expanded = output.column() as usize;
261 let (collapsed, expanded_char_column, to_next_stop) =
262 self.collapse_tabs(chars, expanded, bias);
263 (
264 SuggestionPoint::new(output.row(), collapsed as u32),
265 expanded_char_column,
266 to_next_stop,
267 )
268 }
269
270 pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
271 let fold_point = self
272 .suggestion_snapshot
273 .fold_snapshot
274 .to_fold_point(point, bias);
275 let suggestion_point = self.suggestion_snapshot.to_suggestion_point(fold_point);
276 self.to_tab_point(suggestion_point)
277 }
278
279 pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
280 let suggestion_point = self.to_suggestion_point(point, bias).0;
281 let fold_point = self.suggestion_snapshot.to_fold_point(suggestion_point);
282 fold_point.to_buffer_point(&self.suggestion_snapshot.fold_snapshot)
283 }
284
285 pub fn expand_tabs(&self, chars: impl Iterator<Item = char>, column: usize) -> usize {
286 let tab_size = self.tab_size.get() as usize;
287
288 let mut expanded_chars = 0;
289 let mut expanded_bytes = 0;
290 let mut collapsed_bytes = 0;
291 for c in chars {
292 if collapsed_bytes == column {
293 break;
294 }
295 if c == '\t' {
296 let tab_len = tab_size - expanded_chars % tab_size;
297 expanded_bytes += tab_len;
298 expanded_chars += tab_len;
299 } else {
300 expanded_bytes += c.len_utf8();
301 expanded_chars += 1;
302 }
303 collapsed_bytes += c.len_utf8();
304 }
305 expanded_bytes
306 }
307
308 fn collapse_tabs(
309 &self,
310 chars: impl Iterator<Item = char>,
311 column: usize,
312 bias: Bias,
313 ) -> (usize, usize, usize) {
314 let tab_size = self.tab_size.get() as usize;
315
316 let mut expanded_bytes = 0;
317 let mut expanded_chars = 0;
318 let mut collapsed_bytes = 0;
319 for c in chars {
320 if expanded_bytes >= column {
321 break;
322 }
323
324 if c == '\t' {
325 let tab_len = tab_size - (expanded_chars % tab_size);
326 expanded_chars += tab_len;
327 expanded_bytes += tab_len;
328 if expanded_bytes > column {
329 expanded_chars -= expanded_bytes - column;
330 return match bias {
331 Bias::Left => (collapsed_bytes, expanded_chars, expanded_bytes - column),
332 Bias::Right => (collapsed_bytes + 1, expanded_chars, 0),
333 };
334 }
335 } else {
336 expanded_chars += 1;
337 expanded_bytes += c.len_utf8();
338 }
339
340 if expanded_bytes > column && matches!(bias, Bias::Left) {
341 expanded_chars -= 1;
342 break;
343 }
344
345 collapsed_bytes += c.len_utf8();
346 }
347 (collapsed_bytes, expanded_chars, 0)
348 }
349}
350
351#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
352pub struct TabPoint(pub Point);
353
354impl TabPoint {
355 pub fn new(row: u32, column: u32) -> Self {
356 Self(Point::new(row, column))
357 }
358
359 pub fn zero() -> Self {
360 Self::new(0, 0)
361 }
362
363 pub fn row(self) -> u32 {
364 self.0.row
365 }
366
367 pub fn column(self) -> u32 {
368 self.0.column
369 }
370}
371
372impl From<Point> for TabPoint {
373 fn from(point: Point) -> Self {
374 Self(point)
375 }
376}
377
378pub type TabEdit = text::Edit<TabPoint>;
379
380#[derive(Clone, Debug, Default, Eq, PartialEq)]
381pub struct TextSummary {
382 pub lines: Point,
383 pub first_line_chars: u32,
384 pub last_line_chars: u32,
385 pub longest_row: u32,
386 pub longest_row_chars: u32,
387}
388
389impl<'a> From<&'a str> for TextSummary {
390 fn from(text: &'a str) -> Self {
391 let sum = text::TextSummary::from(text);
392
393 TextSummary {
394 lines: sum.lines,
395 first_line_chars: sum.first_line_chars,
396 last_line_chars: sum.last_line_chars,
397 longest_row: sum.longest_row,
398 longest_row_chars: sum.longest_row_chars,
399 }
400 }
401}
402
403impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
404 fn add_assign(&mut self, other: &'a Self) {
405 let joined_chars = self.last_line_chars + other.first_line_chars;
406 if joined_chars > self.longest_row_chars {
407 self.longest_row = self.lines.row;
408 self.longest_row_chars = joined_chars;
409 }
410 if other.longest_row_chars > self.longest_row_chars {
411 self.longest_row = self.lines.row + other.longest_row;
412 self.longest_row_chars = other.longest_row_chars;
413 }
414
415 if self.lines.row == 0 {
416 self.first_line_chars += other.first_line_chars;
417 }
418
419 if other.lines.row == 0 {
420 self.last_line_chars += other.first_line_chars;
421 } else {
422 self.last_line_chars = other.last_line_chars;
423 }
424
425 self.lines += &other.lines;
426 }
427}
428
429// Handles a tab width <= 16
430const SPACES: &str = " ";
431
432pub struct TabChunks<'a> {
433 suggestion_chunks: SuggestionChunks<'a>,
434 chunk: Chunk<'a>,
435 column: usize,
436 output_position: Point,
437 max_output_position: Point,
438 tab_size: NonZeroU32,
439 skip_leading_tab: bool,
440}
441
442impl<'a> Iterator for TabChunks<'a> {
443 type Item = Chunk<'a>;
444
445 fn next(&mut self) -> Option<Self::Item> {
446 if self.chunk.text.is_empty() {
447 if let Some(chunk) = self.suggestion_chunks.next() {
448 self.chunk = chunk;
449 if self.skip_leading_tab {
450 self.chunk.text = &self.chunk.text[1..];
451 self.skip_leading_tab = false;
452 }
453 } else {
454 return None;
455 }
456 }
457
458 for (ix, c) in self.chunk.text.char_indices() {
459 match c {
460 '\t' => {
461 if ix > 0 {
462 let (prefix, suffix) = self.chunk.text.split_at(ix);
463 self.chunk.text = suffix;
464 return Some(Chunk {
465 text: prefix,
466 ..self.chunk
467 });
468 } else {
469 self.chunk.text = &self.chunk.text[1..];
470 let tab_size = self.tab_size.get() as u32;
471 let mut len = tab_size - self.column as u32 % tab_size;
472 let next_output_position = cmp::min(
473 self.output_position + Point::new(0, len),
474 self.max_output_position,
475 );
476 len = next_output_position.column - self.output_position.column;
477 self.column += len as usize;
478 self.output_position = next_output_position;
479 return Some(Chunk {
480 text: &SPACES[0..len as usize],
481 ..self.chunk
482 });
483 }
484 }
485 '\n' => {
486 self.column = 0;
487 self.output_position += Point::new(1, 0);
488 }
489 _ => {
490 self.column += 1;
491 self.output_position.column += c.len_utf8() as u32;
492 }
493 }
494 }
495
496 Some(mem::take(&mut self.chunk))
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503 use crate::{
504 display_map::{fold_map::FoldMap, suggestion_map::SuggestionMap},
505 MultiBuffer,
506 };
507 use rand::{prelude::StdRng, Rng};
508
509 #[gpui::test]
510 fn test_expand_tabs(cx: &mut gpui::MutableAppContext) {
511 let buffer = MultiBuffer::build_simple("", cx);
512 let buffer_snapshot = buffer.read(cx).snapshot(cx);
513 let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
514 let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
515 let (_, tabs_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
516
517 assert_eq!(tabs_snapshot.expand_tabs("\t".chars(), 0), 0);
518 assert_eq!(tabs_snapshot.expand_tabs("\t".chars(), 1), 4);
519 assert_eq!(tabs_snapshot.expand_tabs("\ta".chars(), 2), 5);
520 }
521
522 #[gpui::test(iterations = 100)]
523 fn test_random_tabs(cx: &mut gpui::MutableAppContext, mut rng: StdRng) {
524 let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
525 let len = rng.gen_range(0..30);
526 let buffer = if rng.gen() {
527 let text = util::RandomCharIter::new(&mut rng)
528 .take(len)
529 .collect::<String>();
530 MultiBuffer::build_simple(&text, cx)
531 } else {
532 MultiBuffer::build_random(&mut rng, cx)
533 };
534 let buffer_snapshot = buffer.read(cx).snapshot(cx);
535 log::info!("Buffer text: {:?}", buffer_snapshot.text());
536
537 let (mut fold_map, _) = FoldMap::new(buffer_snapshot.clone());
538 fold_map.randomly_mutate(&mut rng);
539 let (fold_snapshot, _) = fold_map.read(buffer_snapshot, vec![]);
540 log::info!("FoldMap text: {:?}", fold_snapshot.text());
541 let (suggestion_map, _) = SuggestionMap::new(fold_snapshot);
542 let (suggestion_snapshot, _) = suggestion_map.randomly_mutate(&mut rng);
543 log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
544
545 let (_, tabs_snapshot) = TabMap::new(suggestion_snapshot.clone(), tab_size);
546 let text = text::Rope::from(tabs_snapshot.text().as_str());
547 log::info!(
548 "TabMap text (tab size: {}): {:?}",
549 tab_size,
550 tabs_snapshot.text(),
551 );
552
553 for _ in 0..5 {
554 let end_row = rng.gen_range(0..=text.max_point().row);
555 let end_column = rng.gen_range(0..=text.line_len(end_row));
556 let mut end = TabPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right));
557 let start_row = rng.gen_range(0..=text.max_point().row);
558 let start_column = rng.gen_range(0..=text.line_len(start_row));
559 let mut start =
560 TabPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left));
561 if start > end {
562 mem::swap(&mut start, &mut end);
563 }
564
565 let expected_text = text
566 .chunks_in_range(text.point_to_offset(start.0)..text.point_to_offset(end.0))
567 .collect::<String>();
568 let expected_summary = TextSummary::from(expected_text.as_str());
569 assert_eq!(
570 expected_text,
571 tabs_snapshot
572 .chunks(start..end, false, None)
573 .map(|c| c.text)
574 .collect::<String>(),
575 "chunks({:?}..{:?})",
576 start,
577 end
578 );
579
580 let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end);
581 if tab_size.get() > 1 && suggestion_snapshot.text().contains('\t') {
582 actual_summary.longest_row = expected_summary.longest_row;
583 actual_summary.longest_row_chars = expected_summary.longest_row_chars;
584 }
585 assert_eq!(actual_summary, expected_summary);
586 }
587
588 for row in 0..=text.max_point().row {
589 assert_eq!(tabs_snapshot.line_len(row), text.line_len(row));
590 }
591 }
592}