From 35c9567101d85353288a2fcb4d9493305fa6a7fe Mon Sep 17 00:00:00 2001 From: faga Date: Sat, 18 Mar 2023 00:16:05 +0800 Subject: [PATCH 1/3] chore: indent init --- core/src/magic_string.rs | 80 +++++++++++++++++++++++++++++++++++++++- core/src/utils.rs | 41 ++++++++++++++++++++ core/tests/indent.rs | 40 ++++++++++++++++++++ 3 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 core/tests/indent.rs diff --git a/core/src/magic_string.rs b/core/src/magic_string.rs index 325afde..3a2a4d2 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; @@ -72,6 +74,8 @@ pub struct MagicString { last_searched_chunk: Rc>, first_chunk: Rc>, last_chunk: Rc>, + + indent_str: String, } impl MagicString { @@ -104,6 +108,8 @@ impl MagicString { last_searched_chunk: Rc::clone(&original_chunk), original_str_locator: Locator::new(str), + + indent_str: String::default(), } } @@ -525,6 +531,71 @@ 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) -> Result<&mut Self> { + let pattern = Regex::new(r"^[^\r\n]")?; + if self.indent_str.len() == 0 { + self._ensure_indent_str(); + }; + let 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; + while let Some(c) = chunk.clone() { + if c.borrow().is_content_edited() { + c.borrow_mut().content = replacer(c.borrow().content.as_str()); + } + let mut char_index = c.borrow().start; + while char_index < c.borrow().end { + 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 != '\n' && 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 +756,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..f81ee83 --- /dev/null +++ b/core/tests/indent.rs @@ -0,0 +1,40 @@ +#[cfg(test)] + +mod indent { + use magic_string::{MagicString, 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()?; + assert_eq!(s.to_string(), "\tabc\n\tdef\n\tghi\n\tjkl"); + + s.indent()?; + 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()?; + assert_eq!(s.to_string(), " abc\n def\n ghi\n jkl"); + + s.indent()?; + 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()?; + + assert_eq!(s.to_string(), "\tabc\n\t/**\n\t *comment\n\t */"); + Ok(()) + } +} From b8702ac671dc449e42c1fa8c2d155bc96365e2c3 Mon Sep 17 00:00:00 2001 From: faga Date: Sat, 18 Mar 2023 01:28:07 +0800 Subject: [PATCH 2/3] feat: indent_str cutomize --- core/src/magic_string.rs | 17 +++++++++++++---- core/tests/indent.rs | 22 ++++++++++++++++------ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/core/src/magic_string.rs b/core/src/magic_string.rs index 3a2a4d2..1d685f6 100644 --- a/core/src/magic_string.rs +++ b/core/src/magic_string.rs @@ -60,6 +60,12 @@ pub struct DecodedMap { pub mappings: Mappings, } +#[cfg(feature = "node-api")] +#[derive(Debug, Default, Clone)] +pub struct IndentOptions { + pub indent_str: String, +} + #[derive(Debug, Clone)] pub struct MagicString { original_str: String, @@ -542,12 +548,15 @@ impl MagicString { /// /// ``` /// - pub fn indent(&mut self) -> Result<&mut Self> { + pub fn indent(&mut self, option: IndentOptions) -> Result<&mut Self> { + let mut indent_str = option.indent_str; let pattern = Regex::new(r"^[^\r\n]")?; - if self.indent_str.len() == 0 { - self._ensure_indent_str(); + if indent_str.len() == 0 { + if self.indent_str.len() == 0 { + self._ensure_indent_str()?; + } + indent_str = self.indent_str.clone(); }; - let indent_str = self.indent_str.clone(); let replacer = |input: &str| { let mut s = input.to_string(); diff --git a/core/tests/indent.rs b/core/tests/indent.rs index f81ee83..417d683 100644 --- a/core/tests/indent.rs +++ b/core/tests/indent.rs @@ -1,15 +1,15 @@ #[cfg(test)] mod indent { - use magic_string::{MagicString, Result}; + use magic_string::{IndentOptions, MagicString, 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()?; + s.indent(IndentOptions::default())?; assert_eq!(s.to_string(), "\tabc\n\tdef\n\tghi\n\tjkl"); - s.indent()?; + s.indent(IndentOptions::default())?; assert_eq!(s.to_string(), "\t\tabc\n\t\tdef\n\t\tghi\n\t\tjkl"); Ok(()) @@ -19,10 +19,10 @@ mod indent { 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()?; + s.indent(IndentOptions::default())?; assert_eq!(s.to_string(), " abc\n def\n ghi\n jkl"); - s.indent()?; + s.indent(IndentOptions::default())?; assert_eq!(s.to_string(), " abc\n def\n ghi\n jkl"); Ok(()) @@ -32,9 +32,19 @@ mod indent { fn should_disregard_single_space_indentation_when_auto_indenting() -> Result { let mut s = MagicString::new("abc\n/**\n *comment\n */"); - s.indent()?; + 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(), + })?; + assert_eq!(s.to_string(), ">>abc\n>>def\n>>ghi\n>>jkl"); + Ok(()) + } } From a621b7bab0c98f4cfddf1d753ae78e06b120b7ae Mon Sep 17 00:00:00 2001 From: faga Date: Tue, 21 Mar 2023 16:30:07 +0800 Subject: [PATCH 3/3] chore: support exclude --- core/src/magic_string.rs | 88 ++++++++++++++++++++++++++++------------ core/tests/indent.rs | 85 +++++++++++++++++++++++++++++++++++++- 2 files changed, 147 insertions(+), 26 deletions(-) diff --git a/core/src/magic_string.rs b/core/src/magic_string.rs index 1d685f6..d17ef54 100644 --- a/core/src/magic_string.rs +++ b/core/src/magic_string.rs @@ -61,9 +61,26 @@ pub struct DecodedMap { } #[cfg(feature = "node-api")] -#[derive(Debug, Default, Clone)] +#[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)] @@ -550,7 +567,9 @@ impl MagicString { /// 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 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()?; @@ -570,34 +589,53 @@ impl MagicString { 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() { - c.borrow_mut().content = replacer(c.borrow().content.as_str()); - } - let mut char_index = c.borrow().start; - while char_index < c.borrow().end { - 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 != '\n' && 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); + 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; } - char_index += 1; } chunk = c.borrow().next.clone(); } diff --git a/core/tests/indent.rs b/core/tests/indent.rs index 417d683..967d393 100644 --- a/core/tests/indent.rs +++ b/core/tests/indent.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod indent { - use magic_string::{IndentOptions, MagicString, Result}; + 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"); @@ -43,8 +43,91 @@ mod indent { 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(()) + } }