string_wizard/magic_string/
indent.rs1use 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 pub indentor: Option<&'a str>,
55
56 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 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}