Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: indent api #23

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 126 additions & 1 deletion core/src/magic_string.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<u32>,
}

#[cfg(not(feature = "node-api"))]
#[derive(Debug, Clone)]
pub struct IndentOptions {
pub indent_str: String,
pub exclude: Vec<u32>,
}

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,
Expand All @@ -72,6 +97,8 @@ pub struct MagicString {
last_searched_chunk: Rc<RefCell<Chunk>>,
first_chunk: Rc<RefCell<Chunk>>,
last_chunk: Rc<RefCell<Chunk>>,

indent_str: String,
}

impl MagicString {
Expand Down Expand Up @@ -104,6 +131,8 @@ impl MagicString {
last_searched_chunk: Rc::clone(&original_chunk),

original_str_locator: Locator::new(str),

indent_str: String::default(),
}
}

Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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 {
Expand Down
41 changes: 41 additions & 0 deletions core/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ pub mod trim {

use crate::{Error, MagicStringErrorType, Result};

use regex::Regex;

pub fn normalize_index(s: &str, index: i64) -> Result<usize> {
let len = s.len() as i64;

Expand All @@ -163,3 +165,42 @@ pub fn normalize_index(s: &str, index: i64) -> Result<usize> {

Ok(index as usize)
}

pub fn guess_indent(str: &str) -> Result<String> {
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::<Vec<&str>>();
let tabbed = lines
.clone()
.into_iter()
.filter(|line| tab_pattern.is_match(line))
.collect::<Vec<&str>>();

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())
}
133 changes: 133 additions & 0 deletions core/tests/indent.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
}