dprint_swc_ext/common/
text_info.rs

1use std::sync::Arc;
2
3use swc_common::input::StringInput;
4pub use text_lines::LineAndColumnIndex;
5pub use text_lines::TextLines;
6
7use super::pos::*;
8use super::text_encoding::strip_bom_mut;
9use super::text_encoding::BOM_CHAR;
10use super::LineAndColumnDisplay;
11
12/// Stores the source text along with other data such as where all the lines
13/// occur in the text.
14///
15/// Note: This struct is cheap to clone.
16#[derive(Clone)]
17pub struct SourceTextInfo {
18  // keep this struct cheap to clone
19  start_pos: StartSourcePos,
20  text: Arc<str>,
21  text_lines: Arc<TextLines>,
22}
23
24impl SourceTextInfo {
25  /// Creates a new `SourceTextInfo` from the provided source text.
26  pub fn new(text: Arc<str>) -> Self {
27    Self::new_with_pos(StartSourcePos::START_SOURCE_POS.as_source_pos(), text)
28  }
29
30  /// Creates a new `SourceTextInfo` from the provided source start position
31  /// and source text.
32  ///
33  /// Note: When bundling swc will keep increasing the start position for
34  /// each source file.
35  pub fn new_with_pos(start_pos: SourcePos, text: Arc<str>) -> Self {
36    // The BOM should be stripped before it gets passed here
37    // because it's a text encoding concern that should be
38    // stripped when the file is read.
39    // todo(dsherret): re-enable after removing the below
40    // assert!(!text.starts_with(BOM_CHAR), "BOM should be stripped before creating a SourceTextInfo.");
41
42    // todo(dsherret): remove this once handled downstream
43    let text = if text.starts_with(BOM_CHAR) {
44      let mut text = text.to_string();
45      strip_bom_mut(&mut text);
46      text.into()
47    } else {
48      text
49    };
50
51    Self::new_with_indent_width(start_pos, text, 2)
52  }
53
54  /// Creates a new `SourceTextInfo` from the provided start position,
55  /// source text, and indentation width.
56  ///
57  /// The indentation width determines the number of columns to use
58  /// when going over a tab character. For example, an indent width
59  /// of 2 will mean each tab character will represent 2 columns.
60  /// The default indentation width used in the other methods is `2`
61  /// to match the default indentation used by `deno fmt`.
62  pub fn new_with_indent_width(start_pos: SourcePos, text: Arc<str>, indent_width: usize) -> Self {
63    Self {
64      start_pos: StartSourcePos(start_pos),
65      text_lines: Arc::new(TextLines::with_indent_width(&text, indent_width)),
66      text,
67    }
68  }
69
70  /// Creates a new `SourceTextInfo` from the provided source text.
71  ///
72  /// Generally, prefer using `SourceTextInfo::new` to provide a
73  /// string already in an `std::sync::Arc`.
74  pub fn from_string(text: String) -> Self {
75    Self::new(text.into())
76  }
77
78  /// Gets an swc `StringInput` for this text information that can be
79  /// used with parsing.
80  pub fn as_string_input(&self) -> StringInput {
81    let range = self.range();
82    StringInput::new(self.text_str(), range.start.as_byte_pos(), range.end.as_byte_pos())
83  }
84
85  /// Gets the source text.
86  pub fn text(&self) -> Arc<str> {
87    self.text.clone()
88  }
89
90  /// Gets a reference to the source text.
91  pub fn text_str(&self) -> &str {
92    &self.text
93  }
94
95  /// Gets the range—start and end byte position—of the source text.
96  pub fn range(&self) -> SourceRange<StartSourcePos> {
97    SourceRange::new(self.start_pos, self.start_pos + self.text.len())
98  }
99
100  /// Gets the number of lines in the source text.
101  pub fn lines_count(&self) -> usize {
102    self.text_lines.lines_count()
103  }
104
105  /// Gets the 0-indexed line index at the provided byte position.
106  ///
107  /// Note that this will panic when providing a byte position outside
108  /// the range of the source text.
109  pub fn line_index(&self, pos: SourcePos) -> usize {
110    self.assert_pos(pos);
111    self.text_lines.line_index(self.get_relative_index_from_pos(pos))
112  }
113
114  /// Gets the line start byte position of the provided 0-indexed line index.
115  ///
116  /// Note that this will panic if providing a line index outside the
117  /// bounds of the number of lines.
118  pub fn line_start(&self, line_index: usize) -> SourcePos {
119    self.assert_line_index(line_index);
120    self.get_pos_from_relative_index(self.text_lines.line_start(line_index))
121  }
122
123  /// Gets the line end byte position of the provided 0-indexed line index.
124  ///
125  /// Note that this will panic if providing a line index outside the
126  /// bounds of the number of lines.
127  pub fn line_end(&self, line_index: usize) -> SourcePos {
128    self.assert_line_index(line_index);
129    self.get_pos_from_relative_index(self.text_lines.line_end(line_index))
130  }
131
132  /// Gets the 0-indexed line and column index of the provided byte position.
133  ///
134  /// Note that this will panic when providing a byte position outside
135  /// the range of the source text.
136  pub fn line_and_column_index(&self, pos: SourcePos) -> LineAndColumnIndex {
137    self.assert_pos(pos);
138    self.text_lines.line_and_column_index(self.get_relative_index_from_pos(pos))
139  }
140
141  /// Gets the 1-indexed line and column index of the provided byte position
142  /// taking into account the default indentation width.
143  ///
144  /// Note that this will panic when providing a byte position outside
145  /// the range of the source text.
146  pub fn line_and_column_display(&self, pos: SourcePos) -> LineAndColumnDisplay {
147    self.assert_pos(pos);
148    self.text_lines.line_and_column_display(self.get_relative_index_from_pos(pos))
149  }
150
151  /// Gets the 1-indexed line and column index of the provided byte position
152  /// with a custom indentation width.
153  ///
154  /// Note that this will panic when providing a byte position outside
155  /// the range of the source text.
156  pub fn line_and_column_display_with_indent_width(&self, pos: SourcePos, indent_width: usize) -> LineAndColumnDisplay {
157    self.assert_pos(pos);
158    self
159      .text_lines
160      .line_and_column_display_with_indent_width(self.get_relative_index_from_pos(pos), indent_width)
161  }
162
163  /// Gets the source position of the provided line and column index.
164  ///
165  /// Note that this will panic if providing a line index outside the
166  /// bounds of the number of lines, but will clip the the line end byte index
167  /// when exceeding the line length.
168  pub fn loc_to_source_pos(&self, line_and_column_index: LineAndColumnIndex) -> SourcePos {
169    self.assert_line_index(line_and_column_index.line_index);
170    self.get_pos_from_relative_index(self.text_lines.byte_index(line_and_column_index))
171  }
172
173  /// Gets a reference to the text slice of the line at the provided
174  /// 0-based index.
175  ///
176  /// Note that this will panic if providing a line index outside the
177  /// bounds of the number of lines.
178  pub fn line_text(&self, line_index: usize) -> &str {
179    let range = SourceRange {
180      start: self.line_start(line_index),
181      end: self.line_end(line_index),
182    };
183    self.range_text(&range)
184  }
185
186  /// Gets the source text located within the provided range.
187  pub fn range_text(&self, range: &SourceRange) -> &str {
188    let start = self.get_relative_index_from_pos(range.start);
189    let end = self.get_relative_index_from_pos(range.end);
190    &self.text_str()[start..end]
191  }
192
193  fn assert_pos(&self, pos: SourcePos) {
194    let range = self.range();
195    if pos < range.start {
196      panic!("The provided position {} was less than the start position {}.", pos, range.start,);
197    } else if pos > range.end {
198      panic!("The provided position {} was greater than the end position {}.", pos, range.end,);
199    }
200  }
201
202  fn assert_line_index(&self, line_index: usize) {
203    if line_index >= self.lines_count() {
204      panic!(
205        "The specified line index {} was greater or equal to the number of lines of {}.",
206        line_index,
207        self.lines_count()
208      );
209    }
210  }
211
212  fn get_relative_index_from_pos(&self, pos: SourcePos) -> usize {
213    pos - self.start_pos
214  }
215
216  fn get_pos_from_relative_index(&self, relative_index: usize) -> SourcePos {
217    self.start_pos + relative_index
218  }
219}
220
221impl std::fmt::Debug for SourceTextInfo {
222  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223    f.debug_struct("SourceTextInfo")
224      .field("start_pos", &self.start_pos)
225      .field("text", &self.text)
226      .finish()
227  }
228}
229
230pub trait SourceTextInfoProvider<'a> {
231  fn text_info(&self) -> &'a SourceTextInfo;
232}
233
234impl<'a> SourceTextInfoProvider<'a> for &'a SourceTextInfo {
235  fn text_info(&self) -> &'a SourceTextInfo {
236    self
237  }
238}
239
240pub trait SourceTextProvider<'a> {
241  fn text(&self) -> &'a Arc<str>;
242  fn start_pos(&self) -> StartSourcePos;
243}
244
245impl<'a, T> SourceTextProvider<'a> for T
246where
247  T: SourceTextInfoProvider<'a>,
248{
249  fn text(&self) -> &'a Arc<str> {
250    &self.text_info().text
251  }
252
253  fn start_pos(&self) -> StartSourcePos {
254    self.text_info().start_pos
255  }
256}
257
258#[cfg(test)]
259mod test {
260  use super::*;
261
262  #[test]
263  fn line_and_column_index() {
264    let text = "12\n3\r\nβ\n5";
265    for i in 0..10 {
266      run_with_text_info(SourceTextInfo::new_with_pos(SourcePos::new(i), text.to_string().into()), i);
267    }
268
269    fn run_with_text_info(text_info: SourceTextInfo, i: usize) {
270      assert_pos_line_and_col(&text_info, i, 0, 0); // 1
271      assert_pos_line_and_col(&text_info, 1 + i, 0, 1); // 2
272      assert_pos_line_and_col(&text_info, 2 + i, 0, 2); // \n
273      assert_pos_line_and_col(&text_info, 3 + i, 1, 0); // 3
274      assert_pos_line_and_col(&text_info, 4 + i, 1, 1); // \r
275      assert_pos_line_and_col(&text_info, 5 + i, 1, 2); // \n
276      assert_pos_line_and_col(&text_info, 6 + i, 2, 0); // first β index
277      assert_pos_line_and_col(&text_info, 7 + i, 2, 0); // second β index
278      assert_pos_line_and_col(&text_info, 8 + i, 2, 1); // \n
279      assert_pos_line_and_col(&text_info, 9 + i, 3, 0); // 5
280      assert_pos_line_and_col(&text_info, 10 + i, 3, 1); // <EOF>
281    }
282  }
283
284  fn assert_pos_line_and_col(text_info: &SourceTextInfo, pos: usize, line_index: usize, column_index: usize) {
285    assert_eq!(
286      text_info.line_and_column_index(SourcePos::new(pos)),
287      LineAndColumnIndex { line_index, column_index }
288    );
289  }
290
291  #[test]
292  #[should_panic(expected = "The provided position 0 was less than the start position 1.")]
293  fn line_and_column_index_panic_less_than() {
294    let info = SourceTextInfo::new_with_pos(SourcePos::new(1), "test".to_string().into());
295    info.line_and_column_index(SourcePos::new(0));
296  }
297
298  #[test]
299  #[should_panic(expected = "The provided position 6 was greater than the end position 5.")]
300  fn line_and_column_index_panic_greater_than() {
301    let info = SourceTextInfo::new_with_pos(SourcePos::new(1), "test".to_string().into());
302    info.line_and_column_index(SourcePos::new(6));
303  }
304
305  #[test]
306  fn line_start() {
307    let text = "12\n3\r\n4\n5";
308    for i in 0..10 {
309      run_with_text_info(SourceTextInfo::new_with_pos(SourcePos::new(i), text.to_string().into()), i);
310    }
311
312    fn run_with_text_info(text_info: SourceTextInfo, i: usize) {
313      assert_line_start(&text_info, 0, i);
314      assert_line_start(&text_info, 1, 3 + i);
315      assert_line_start(&text_info, 2, 6 + i);
316      assert_line_start(&text_info, 3, 8 + i);
317    }
318  }
319
320  fn assert_line_start(text_info: &SourceTextInfo, line_index: usize, line_end: usize) {
321    assert_eq!(text_info.line_start(line_index), SourcePos::new(line_end));
322  }
323
324  #[test]
325  #[should_panic(expected = "The specified line index 1 was greater or equal to the number of lines of 1.")]
326  fn line_start_equal_number_lines() {
327    let info = SourceTextInfo::new_with_pos(SourcePos::new(1), "test".to_string().into());
328    info.line_start(1);
329  }
330
331  #[test]
332  fn line_end() {
333    let text = "12\n3\r\n4\n5";
334    for i in 0..10 {
335      run_with_text_info(SourceTextInfo::new_with_pos(SourcePos::new(i), text.to_string().into()), i);
336    }
337
338    fn run_with_text_info(text_info: SourceTextInfo, i: usize) {
339      assert_line_end(&text_info, 0, 2 + i);
340      assert_line_end(&text_info, 1, 4 + i);
341      assert_line_end(&text_info, 2, 7 + i);
342      assert_line_end(&text_info, 3, 9 + i);
343    }
344  }
345
346  fn assert_line_end(text_info: &SourceTextInfo, line_index: usize, line_end: usize) {
347    assert_eq!(text_info.line_end(line_index), SourcePos::new(line_end));
348  }
349
350  #[test]
351  #[should_panic(expected = "The specified line index 1 was greater or equal to the number of lines of 1.")]
352  fn line_end_equal_number_lines() {
353    let info = SourceTextInfo::new_with_pos(SourcePos::new(1), "test".to_string().into());
354    info.line_end(1);
355  }
356}