string_wizard/magic_string/
indent.rs

1use std::borrow::Cow;
2
3use crate::{CowStr, MagicString};
4
5struct ExcludeSet<'a> {
6  exclude: &'a [(usize, usize)],
7}
8
9impl<'a> ExcludeSet<'a> {
10  fn new(exclude: &'a [(usize, usize)]) -> Self {
11    Self { exclude }
12  }
13
14  fn contains(&self, index: usize) -> bool {
15    self.exclude.iter().any(|s| s.0 <= index && index < s.1)
16  }
17}
18
19pub fn guess_indentor(source: &str) -> Option<String> {
20  let mut tabbed_count = 0;
21  let mut spaced_line = vec![];
22  for line in source.lines() {
23    if line.starts_with('\t') {
24      tabbed_count += 1;
25    } else if line.starts_with("  ") {
26      spaced_line.push(line);
27    }
28  }
29
30  if tabbed_count == 0 && spaced_line.is_empty() {
31    return None;
32  }
33
34  if tabbed_count >= spaced_line.len() {
35    return Some("\t".to_string());
36  }
37
38  let min_space_count = spaced_line
39    .iter()
40    .map(|line| line.chars().take_while(|c| *c == ' ').count())
41    .min()
42    .unwrap_or(0);
43
44  let mut indent_str = String::with_capacity(min_space_count);
45  for _ in 0..min_space_count {
46    indent_str.push(' ');
47  }
48  Some(indent_str)
49}
50
51#[derive(Debug, Default)]
52pub struct IndentOptions<'a, 'b> {
53  /// MagicString will guess the `indentor` from lines of the source if passed `None`.
54  pub indentor: Option<&'a str>,
55
56  /// The reason I use `[u32; 2]` instead of `(u32, u32)` to represent a range of text is that
57  /// I want to emphasize that the `[u32; 2]` is the closed interval, which means both the start
58  /// and the end are included in the range.
59  pub exclude: &'b [(usize, usize)],
60}
61
62impl MagicString<'_> {
63  fn guessed_indentor(&mut self) -> &str {
64    let guessed_indentor = self
65      .guessed_indentor
66      .get_or_init(|| guess_indentor(&self.source).unwrap_or_else(|| "\t".to_string()));
67    guessed_indentor
68  }
69
70  pub fn indent(&mut self) -> &mut Self {
71    self.indent_with(IndentOptions { indentor: None, ..Default::default() })
72  }
73
74  pub fn indent_with(&mut self, opts: IndentOptions) -> &mut Self {
75    if opts.indentor.is_some_and(|s| s.is_empty()) {
76      return self;
77    }
78    struct IndentReplacer {
79      should_indent_next_char: bool,
80      indentor: String,
81    }
82
83    fn indent_frag(frag: &mut CowStr, indent_replacer: &mut IndentReplacer) {
84      let mut indented = String::new();
85      for char in frag.chars() {
86        if char == '\n' {
87          indent_replacer.should_indent_next_char = true;
88        } else if char != '\r' && indent_replacer.should_indent_next_char {
89          indent_replacer.should_indent_next_char = false;
90          indented.push_str(&indent_replacer.indentor);
91        }
92        indented.push(char);
93      }
94      *frag = Cow::Owned(indented);
95    }
96
97    let indentor = opts.indentor.unwrap_or_else(|| self.guessed_indentor());
98
99    let mut indent_replacer =
100      IndentReplacer { should_indent_next_char: true, indentor: indentor.to_string() };
101
102    for intro_frag in self.intro.iter_mut() {
103      indent_frag(intro_frag, &mut indent_replacer)
104    }
105
106    let exclude_set = ExcludeSet::new(opts.exclude);
107
108    let mut next_chunk_id = Some(self.first_chunk_idx);
109    let mut char_index = 0;
110    while let Some(chunk_idx) = next_chunk_id {
111      // Make sure the `next_chunk_id` is updated before we split the chunk. Otherwise, we
112      // might process the same chunk twice.
113      next_chunk_id = self.chunks[chunk_idx].next;
114      if let Some(edited_content) = self.chunks[chunk_idx].edited_content.as_mut() {
115        if !exclude_set.contains(char_index) {
116          indent_frag(edited_content, &mut indent_replacer);
117        }
118      } else {
119        let chunk = &self.chunks[chunk_idx];
120        let mut line_starts = vec![];
121        char_index = chunk.start();
122        let chunk_end = chunk.end();
123        for char in chunk.span.text(&self.source).chars() {
124          debug_assert!(self.source.is_char_boundary(char_index));
125          if !exclude_set.contains(char_index) {
126            if char == '\n' {
127              indent_replacer.should_indent_next_char = true;
128            } else if char != '\r' && indent_replacer.should_indent_next_char {
129              indent_replacer.should_indent_next_char = false;
130              debug_assert!(!line_starts.contains(&char_index));
131              line_starts.push(char_index);
132            }
133          }
134          char_index += char.len_utf8();
135        }
136        for line_start in line_starts {
137          self.prepend_right(line_start, indent_replacer.indentor.clone());
138        }
139        char_index = chunk_end;
140      }
141    }
142
143    for frag in self.outro.iter_mut() {
144      indent_frag(frag, &mut indent_replacer)
145    }
146
147    self
148  }
149}