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