diff --git a/core/src/magic_string.rs b/core/src/magic_string.rs index 325afde..d17ef54 100644 --- a/core/src/magic_string.rs +++ b/core/src/magic_string.rs @@ -1,6 +1,8 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc, string::ToString}; -use crate::utils::{normalize_index, trim}; +use crate::utils::{guess_indent, normalize_index, trim}; + +use regex::Regex; #[cfg(feature = "node-api")] use napi_derive::napi; @@ -58,6 +60,29 @@ pub struct DecodedMap { pub mappings: Mappings, } +#[cfg(feature = "node-api")] +#[derive(Debug, Clone)] +pub struct IndentOptions { + pub indent_str: String, + pub exclude: Vec, +} + +#[cfg(not(feature = "node-api"))] +#[derive(Debug, Clone)] +pub struct IndentOptions { + pub indent_str: String, + pub exclude: Vec, +} + +impl Default for IndentOptions { + fn default() -> Self { + Self { + indent_str: String::from(""), + exclude: vec![u32::MAX, u32::MAX], + } + } +} + #[derive(Debug, Clone)] pub struct MagicString { original_str: String, @@ -72,6 +97,8 @@ pub struct MagicString { last_searched_chunk: Rc>, first_chunk: Rc>, last_chunk: Rc>, + + indent_str: String, } impl MagicString { @@ -104,6 +131,8 @@ impl MagicString { last_searched_chunk: Rc::clone(&original_chunk), original_str_locator: Locator::new(str), + + indent_str: String::default(), } } @@ -525,6 +554,95 @@ impl MagicString { Ok(self) } + /// ## Indent + /// Indents the string by the given number of spaces. Returns `self`. + /// + /// Example: + /// ``` + /// use magic_string::MagicString; + /// + /// let mut s = MagicString::new("abc\ndef\nghi\njkl"); + /// + /// ``` + /// + pub fn indent(&mut self, option: IndentOptions) -> Result<&mut Self> { + let mut indent_str = option.indent_str; + let pattern = Regex::new(r"^\r\n")?; + let exclude_start = option.exclude[0]; + let exclude_end = option.exclude[1]; + if indent_str.len() == 0 { + if self.indent_str.len() == 0 { + self._ensure_indent_str()?; + } + indent_str = self.indent_str.clone(); + }; + + let replacer = |input: &str| { + let mut s = input.to_string(); + pattern.find_iter(input).for_each(|m| { + let start = m.start(); + let end = m.end(); + s.replace_range(start..end, format!("{}{}", indent_str, m.as_str()).as_str()); + }); + s + }; + self.intro = replacer(&self.intro); + let mut chunk = Some(Rc::clone(&self.first_chunk)); + let mut should_indent_next_character = true; + let mut char_index = 0; + while let Some(c) = chunk.clone() { + if c.borrow().is_content_edited() { + if !(char_index >= exclude_start && char_index <= exclude_end) { + let content = c.borrow().content.to_string(); + c.borrow_mut().content = replacer(&content); + if content.len() != 0 { + should_indent_next_character = c + .borrow() + .content + .as_str() + .chars() + .nth(content.len() - 1) + .unwrap() + == '\n'; + } + } + } else { + char_index = c.borrow().start; + while char_index < c.borrow().end { + if char_index >= exclude_start && char_index <= exclude_end { + char_index += 1; + continue; + } + let char = self + .original_str + .as_str() + .chars() + .nth(char_index as usize) + .unwrap(); + if char == '\n' { + should_indent_next_character = true; + } else if char != '\r' && should_indent_next_character { + should_indent_next_character = false; + if char_index == c.borrow().start { + c.borrow_mut().prepend_intro(&indent_str); + } else { + self._split_at_index(char_index)?; + let next_chunk = c.borrow().next.clone(); + chunk = next_chunk.clone(); + if let Some(next_chunk) = next_chunk.clone() { + next_chunk.borrow_mut().prepend_intro(&indent_str); + } + } + } + char_index += 1; + } + } + chunk = c.borrow().next.clone(); + } + + self.outro = replacer(&self.outro); + Ok(self) + } /// ## Is empty /// /// Returns `true` if the resulting source is empty (disregarding white space). @@ -685,6 +803,13 @@ impl MagicString { Ok(()) } + + pub fn _ensure_indent_str(&mut self) -> Result { + if self.indent_str.len() == 0 { + self.indent_str = guess_indent(&self.original_str)? + } + Ok(()) + } } impl ToString for MagicString { diff --git a/core/src/utils.rs b/core/src/utils.rs index 1ecca08..4819b52 100644 --- a/core/src/utils.rs +++ b/core/src/utils.rs @@ -149,6 +149,8 @@ pub mod trim { use crate::{Error, MagicStringErrorType, Result}; +use regex::Regex; + pub fn normalize_index(s: &str, index: i64) -> Result { let len = s.len() as i64; @@ -163,3 +165,42 @@ pub fn normalize_index(s: &str, index: i64) -> Result { Ok(index as usize) } + +pub fn guess_indent(str: &str) -> Result { + let lines: Vec<&str> = str.split('\n').collect(); + + let tab_pattern = Regex::new(r"^\t+")?; + let space_pattern = Regex::new(r"^ {2,}")?; + + let spaced = lines + .clone() + .into_iter() + .filter(|line| space_pattern.is_match(line)) + .collect::>(); + let tabbed = lines + .clone() + .into_iter() + .filter(|line| tab_pattern.is_match(line)) + .collect::>(); + + if tabbed.len() == 0 && spaced.len() == 0 || tabbed.len() > spaced.len() { + return Ok("\t".to_string()); + } + + let mut min: usize = 2 ^ 32; + for space_line in spaced { + let mut space_count = 0; + for c in space_line.chars() { + if c == ' ' { + space_count += 1; + } else { + break; + } + } + + if space_count < min { + min = space_count + } + } + Ok(" ".repeat(min).to_string()) +} diff --git a/core/tests/indent.rs b/core/tests/indent.rs new file mode 100644 index 0000000..967d393 --- /dev/null +++ b/core/tests/indent.rs @@ -0,0 +1,133 @@ +#[cfg(test)] + +mod indent { + use magic_string::{IndentOptions, MagicString, OverwriteOptions, Result}; + #[test] + fn should_indent_content_with_a_single_tab_character_by_default() -> Result { + let mut s = MagicString::new("abc\ndef\nghi\njkl"); + + s.indent(IndentOptions::default())?; + assert_eq!(s.to_string(), "\tabc\n\tdef\n\tghi\n\tjkl"); + + s.indent(IndentOptions::default())?; + assert_eq!(s.to_string(), "\t\tabc\n\t\tdef\n\t\tghi\n\t\tjkl"); + + Ok(()) + } + + #[test] + fn should_indent_content_using_existing_indentation_as_a_guide() -> Result { + let mut s = MagicString::new("abc\n def\n ghi\n jkl"); + + s.indent(IndentOptions::default())?; + assert_eq!(s.to_string(), " abc\n def\n ghi\n jkl"); + + s.indent(IndentOptions::default())?; + assert_eq!(s.to_string(), " abc\n def\n ghi\n jkl"); + + Ok(()) + } + + #[test] + fn should_disregard_single_space_indentation_when_auto_indenting() -> Result { + let mut s = MagicString::new("abc\n/**\n *comment\n */"); + + s.indent(IndentOptions::default())?; + + assert_eq!(s.to_string(), "\tabc\n\t/**\n\t *comment\n\t */"); + Ok(()) + } + + #[test] + fn should_indent_content_using_the_supplied_indent_string() -> Result { + let mut s = MagicString::new("abc\ndef\nghi\njkl"); + s.indent(IndentOptions { + indent_str: ">>".to_string(), + ..IndentOptions::default() + })?; + assert_eq!(s.to_string(), ">>abc\n>>def\n>>ghi\n>>jkl"); + Ok(()) + } + + #[test] + fn should_prevent_excluded_characters_from_being_indented() -> Result { + let mut s = MagicString::new("abc\ndef\nghi\njkl"); + + s.indent(IndentOptions { + indent_str: String::from(" "), + exclude: vec![7, 15], + })?; + assert_eq!(s.to_string(), " abc\n def\nghi\njkl"); + s.indent(IndentOptions { + indent_str: String::from(">>"), + exclude: vec![7, 15], + })?; + assert_eq!(s.to_string(), ">> abc\n>> def\nghi\njkl"); + Ok(()) + } + + #[test] + fn should_not_add_characters_to_empty_line() -> Result { + let mut s = MagicString::new("\n\nabc\ndef\n\nghi\njkl"); + + s.indent(IndentOptions::default())?; + assert_eq!(s.to_string(), "\n\n\tabc\n\tdef\n\n\tghi\n\tjkl"); + + s.indent(IndentOptions::default())?; + assert_eq!(s.to_string(), "\n\n\t\tabc\n\t\tdef\n\n\t\tghi\n\t\tjkl"); + Ok(()) + } + + #[test] + fn should_not_add_characters_to_empty_lines_even_on_windows() -> Result { + let mut s = MagicString::new("\r\n\r\nabc\r\ndef\r\n\r\nghi\r\njkl"); + + s.indent(IndentOptions::default())?; + assert_eq!( + s.to_string(), + "\r\n\r\n\tabc\r\n\tdef\r\n\r\n\tghi\r\n\tjkl" + ); + + s.indent(IndentOptions::default())?; + assert_eq!( + s.to_string(), + "\r\n\r\n\t\tabc\r\n\t\tdef\r\n\r\n\t\tghi\r\n\t\tjkl" + ); + Ok(()) + } + + #[test] + fn should_indent_content_with_removals() -> Result { + let mut s = MagicString::new("/* remove this line */\nvar foo = 1;"); + + s.remove(0, 23)?; + s.indent(IndentOptions::default())?; + + assert_eq!(s.to_string(), "\tvar foo = 1;"); + Ok(()) + } + + #[test] + fn should_not_indent_patches_in_the_middle_of_a_line() -> Result { + let mut s = MagicString::new("class Foo extends Bar {}"); + + s.overwrite(18, 21, "Baz", OverwriteOptions::default())?; + assert_eq!(s.to_string(), "class Foo extends Baz {}"); + + s.indent(IndentOptions::default())?; + assert_eq!(s.to_string(), "\tclass Foo extends Baz {}"); + Ok(()) + } + + #[test] + fn should_return_self() -> Result { + let mut s = MagicString::new("abcdefghijkl"); + let result = s.indent(IndentOptions::default())?; + + let result_ptr = result as *mut _; + let s_ptr = &s as *const _; + + assert_eq!(s_ptr, result_ptr); + Ok(()) + } +}