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