From 16ec5a51467fa56063ba4f849e1c1612636cee36 Mon Sep 17 00:00:00 2001 From: Yunfei He Date: Sun, 25 Aug 2024 16:15:36 +0800 Subject: [PATCH] feat: rework of API and internal structures (#21) * tweak * fmt * tweak2 * Remove once cell * Remove fields * Fix empty string sourcemap issue * feature-gate-sourcemap * tweak * update deps * bump version --- .editorconfig | 11 + Cargo.toml | 9 +- benches/joiner_join.rs | 80 ++- examples/source_map.rs | 29 +- rustfmt.toml | 7 + src/basic_types.rs | 16 - src/chunk.rs | 173 +++---- src/joiner.rs | 99 ++-- src/lib.rs | 14 +- src/locator.rs | 82 --- src/magic_string/append.rs | 52 ++ src/magic_string/indent.rs | 233 +++++---- src/magic_string/mod.rs | 464 +++++++---------- src/magic_string/movement.rs | 86 ++++ src/magic_string/mutation.rs | 97 ---- src/magic_string/prepend.rs | 36 ++ src/magic_string/source_map.rs | 67 ++- src/magic_string/update.rs | 127 +++-- src/source_map/locator.rs | 84 +++ src/source_map/mod.rs | 2 + src/source_map/sourcemap_builder.rs | 104 ++++ src/sourcemap_builder.rs | 104 ---- src/span.rs | 26 +- src/type_aliases.rs | 5 + tests/joiner.rs | 34 +- tests/magic_string.rs | 768 +++++++++++++--------------- tests/magic_string_source_map.rs | 51 +- tests/usage.rs | 14 - 28 files changed, 1373 insertions(+), 1501 deletions(-) create mode 100644 .editorconfig create mode 100644 rustfmt.toml delete mode 100644 src/basic_types.rs delete mode 100644 src/locator.rs create mode 100644 src/magic_string/append.rs create mode 100644 src/magic_string/movement.rs delete mode 100644 src/magic_string/mutation.rs create mode 100644 src/magic_string/prepend.rs create mode 100644 src/source_map/locator.rs create mode 100644 src/source_map/mod.rs create mode 100644 src/source_map/sourcemap_builder.rs delete mode 100644 src/sourcemap_builder.rs create mode 100644 src/type_aliases.rs delete mode 100644 tests/usage.rs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e413173 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false diff --git a/Cargo.toml b/Cargo.toml index b10cee6..e7c27a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,21 +1,20 @@ [package] name = "string_wizard" -version = "0.0.21" +version = "0.0.22" edition = "2021" license = "MIT" -description = "manipulate string like wizards" +description = "manipulate string like a wizard" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] index_vec = { version = "0.1.3" } rustc-hash = { version = "1.1.0" } -once_cell = "1.18.0" -oxc_sourcemap = { version = "~0.15.0" } +oxc_sourcemap = { version = "0.25.0", optional = true} [features] # Enable source map functionality -source_map = [] +source_map = ['dep:oxc_sourcemap'] [dev-dependencies] glob = "0.3.1" diff --git a/benches/joiner_join.rs b/benches/joiner_join.rs index 69b9ca1..3fae361 100644 --- a/benches/joiner_join.rs +++ b/benches/joiner_join.rs @@ -1,54 +1,46 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; fn get_bunch_of_strings() -> Vec { - let files = glob::glob("fixtures/threejs_src/**/*.js").unwrap(); - let mut files = files - .into_iter() - .map(|p| p.unwrap().canonicalize().unwrap()) - .collect::>(); - files.sort(); - let stirngs = files - .iter() - .map(|p| std::fs::read_to_string(p).unwrap()) - .collect::>(); + let files = glob::glob("fixtures/threejs_src/**/*.js").unwrap(); + let mut files = files.into_iter().map(|p| p.unwrap().canonicalize().unwrap()).collect::>(); + files.sort(); + let stirngs = files.iter().map(|p| std::fs::read_to_string(p).unwrap()).collect::>(); - let mut ret = vec![]; - for _ in 0..10 { - ret.extend(stirngs.clone()); - } - ret + let mut ret = vec![]; + for _ in 0..10 { + ret.extend(stirngs.clone()); + } + ret } fn criterion_benchmark(c: &mut Criterion) { - let bunch_of_strings = get_bunch_of_strings(); - - let mut joiner = string_wizard::Joiner::new(); - bunch_of_strings.clone().into_iter().for_each(|s| { - joiner.append_raw(s); - }); - c.bench_function("Joiner#join", |b| b.iter(|| black_box(joiner.join()))); - c.bench_function("Vec#concat", |b| { - b.iter(|| black_box(bunch_of_strings.concat())) - }); - c.bench_function("manual_push", |b| { - b.iter(|| { - let mut output = String::new(); - bunch_of_strings.iter().for_each(|s| { - output.push_str(s); - }); - black_box(output) - }) - }); - c.bench_function("manual_push_with_cap", |b| { - b.iter(|| { - let cap: usize = bunch_of_strings.iter().map(|s| s.len()).sum(); - let mut output = String::with_capacity(cap); - bunch_of_strings.iter().for_each(|s| { - output.push_str(s); - }); - black_box(output) - }) - }); + let bunch_of_strings = get_bunch_of_strings(); + + let mut joiner = string_wizard::Joiner::new(); + bunch_of_strings.clone().into_iter().for_each(|s| { + joiner.append_raw(s); + }); + c.bench_function("Joiner#join", |b| b.iter(|| black_box(joiner.join()))); + c.bench_function("Vec#concat", |b| b.iter(|| black_box(bunch_of_strings.concat()))); + c.bench_function("manual_push", |b| { + b.iter(|| { + let mut output = String::new(); + bunch_of_strings.iter().for_each(|s| { + output.push_str(s); + }); + black_box(output) + }) + }); + c.bench_function("manual_push_with_cap", |b| { + b.iter(|| { + let cap: usize = bunch_of_strings.iter().map(|s| s.len()).sum(); + let mut output = String::with_capacity(cap); + bunch_of_strings.iter().for_each(|s| { + output.push_str(s); + }); + black_box(output) + }) + }); } criterion_group!(benches, criterion_benchmark); diff --git a/examples/source_map.rs b/examples/source_map.rs index 31df41d..5ef7f97 100644 --- a/examples/source_map.rs +++ b/examples/source_map.rs @@ -1,26 +1,19 @@ use string_wizard::{MagicString, SourceMapOptions, UpdateOptions}; fn main() { - let demo = "
\n hello, world\n
"; - let mut s = MagicString::new(demo); + let demo = "
\n hello, world\n
"; + let mut s = MagicString::new(demo); - let update_options = UpdateOptions { - keep_original: true, - ..Default::default() - }; - s.prepend("import React from 'react';\n") - .update_with(1, 2, "v", update_options.clone()) - .update_with(3, 4, "d", update_options.clone()) - .update_with(demo.len() - 4, demo.len() - 1, "h1", update_options.clone()); + let update_options = UpdateOptions { keep_original: true, ..Default::default() }; + s.prepend("import React from 'react';\n") + .update_with(1, 2, "v", update_options.clone()) + .update_with(3, 4, "d", update_options.clone()) + .update_with(demo.len() - 4, demo.len() - 1, "h1", update_options.clone()); - let sm = s.source_map(SourceMapOptions { - include_content: true, - ..Default::default() - }); + let sm = s.source_map(SourceMapOptions { include_content: true, ..Default::default() }); - std::fs::write("./demo.map.json", sm.to_json_string().unwrap()) - .unwrap(); - std::fs::write("./demo.jsx", s.to_string()).unwrap(); + std::fs::write("./demo.map.json", sm.to_json_string()).unwrap(); + std::fs::write("./demo.jsx", s.to_string()).unwrap(); - println!("{:#?}", s.to_string()); + println!("{:#?}", s.to_string()); } diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..21c0fbc --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,7 @@ +tab_spaces = 2 + +# This is also the setting used by [rustc](https://github.com/rust-lang/rust/blob/master/rustfmt.toml) +use_small_heuristics = "Max" + +# Use field initialize shorthand if possible +use_field_init_shorthand = true diff --git a/src/basic_types.rs b/src/basic_types.rs deleted file mode 100644 index fdd2c14..0000000 --- a/src/basic_types.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std::fmt::Debug; - -// This is basically doing the same thing as `TryInto`. -// If we use `TryInto`, we need to put `where >::Error: Debug` everywhere. -pub trait AssertIntoU32 { - fn assert_into_u32(self) -> u32; -} - -impl> AssertIntoU32 for T -where - >::Error: Debug, -{ - fn assert_into_u32(self) -> u32 { - self.try_into().unwrap() - } -} \ No newline at end of file diff --git a/src/chunk.rs b/src/chunk.rs index 4f68bef..379744d 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -1,128 +1,113 @@ use std::collections::VecDeque; -use index_vec::IndexVec; - -use crate::{span::Span, CowStr, TextSize}; +use crate::{span::Span, CowStr}; index_vec::define_index_type! { pub struct ChunkIdx = u32; } -pub type ChunkVec<'s> = IndexVec>; - #[derive(Debug)] pub struct EditOptions { - /// `true` will clear the `intro` and `outro` of the [Chunk] - pub overwrite: bool, - pub store_name: bool, + /// `true` will clear the `intro` and `outro` of the [Chunk] + pub overwrite: bool, + pub store_name: bool, } impl Default for EditOptions { - fn default() -> Self { - Self { - overwrite: true, - store_name: false, - } - } + fn default() -> Self { + Self { overwrite: true, store_name: false } + } } #[derive(Debug, Default, Clone)] pub struct Chunk<'str> { - pub intro: VecDeque>, - pub outro: VecDeque>, - pub span: Span, - pub edited_content: Option>, - pub next: Option, - pub prev: Option, - pub keep_in_mappings: bool, + pub intro: VecDeque>, + pub outro: VecDeque>, + pub span: Span, + pub edited_content: Option>, + pub next: Option, + pub prev: Option, + pub keep_in_mappings: bool, } impl<'s> Chunk<'s> { - pub fn new(span: Span) -> Self { - Self { - span, - ..Default::default() - } - } + pub fn new(span: Span) -> Self { + Self { span, ..Default::default() } + } } impl<'str> Chunk<'str> { - pub fn start(&self) -> TextSize { - self.span.start() - } + pub fn start(&self) -> usize { + self.span.start() + } - pub fn end(&self) -> TextSize { - self.span.end() - } + pub fn end(&self) -> usize { + self.span.end() + } - pub fn contains(&self, text_index: TextSize) -> bool { - self.start() < text_index && text_index < self.end() - } + pub fn contains(&self, text_index: usize) -> bool { + self.start() < text_index && text_index < self.end() + } - pub fn append_outro(&mut self, content: CowStr<'str>) { - self.outro.push_back(content) - } + pub fn append_outro(&mut self, content: CowStr<'str>) { + self.outro.push_back(content) + } - pub fn append_intro(&mut self, content: CowStr<'str>) { - self.intro.push_back(content) - } + pub fn append_intro(&mut self, content: CowStr<'str>) { + self.intro.push_back(content) + } - pub fn prepend_outro(&mut self, content: CowStr<'str>) { - self.outro.push_front(content) - } + pub fn prepend_outro(&mut self, content: CowStr<'str>) { + self.outro.push_front(content) + } - pub fn prepend_intro(&mut self, content: CowStr<'str>) { - self.intro.push_front(content) - } + pub fn prepend_intro(&mut self, content: CowStr<'str>) { + self.intro.push_front(content) + } - pub fn split<'a>(&'a mut self, text_index: TextSize) -> Chunk<'str> { - if !(text_index > self.start() && text_index < self.end()) { - panic!("Cannot split chunk at {text_index} between {:?}", self.span); - } - if self.edited_content.is_some() { - panic!("Cannot split a chunk that has already been edited") - } - let first_half_slice = Span(self.start(), text_index); - let second_half_slice = Span(text_index, self.end()); - let mut new_chunk = Chunk::new(second_half_slice); - if self.is_edited() { - new_chunk.edit( - "".into(), - EditOptions { - store_name: self.keep_in_mappings, - overwrite: false, - }, - ); - } - std::mem::swap(&mut new_chunk.outro, &mut self.outro); - self.span = first_half_slice; - new_chunk + pub fn split<'a>(&'a mut self, text_index: usize) -> Chunk<'str> { + if !(text_index > self.start() && text_index < self.end()) { + panic!("Cannot split chunk at {text_index} between {:?}", self.span); } - - pub fn fragments( - &'str self, - original_source: &'str CowStr<'str>, - ) -> impl Iterator { - let intro_iter = self.intro.iter().map(|frag| frag.as_ref()); - let source_frag = self - .edited_content - .as_ref() - .map(|s| s.as_ref()) - .unwrap_or_else(|| self.span.text(&original_source)); - let outro_iter = self.outro.iter().map(|frag| frag.as_ref()); - intro_iter.chain(Some(source_frag)).chain(outro_iter) + if self.edited_content.is_some() { + panic!("Cannot split a chunk that has already been edited") } - - pub fn edit(&mut self, content: CowStr<'str>, opts: EditOptions) { - if opts.overwrite { - self.intro.clear(); - self.outro.clear(); - } - self.keep_in_mappings = opts.store_name; - self.edited_content = Some(content); + let first_half_slice = Span(self.start(), text_index); + let second_half_slice = Span(text_index, self.end()); + let mut new_chunk = Chunk::new(second_half_slice); + if self.is_edited() { + new_chunk + .edit("".into(), EditOptions { store_name: self.keep_in_mappings, overwrite: false }); } - - pub fn is_edited(&self) -> bool { - self.edited_content.is_some() + std::mem::swap(&mut new_chunk.outro, &mut self.outro); + self.span = first_half_slice; + new_chunk + } + + pub fn fragments( + &'str self, + original_source: &'str CowStr<'str>, + ) -> impl Iterator { + let intro_iter = self.intro.iter().map(|frag| frag.as_ref()); + let source_frag = self + .edited_content + .as_ref() + .map(|s| s.as_ref()) + .unwrap_or_else(|| self.span.text(&original_source)); + let outro_iter = self.outro.iter().map(|frag| frag.as_ref()); + intro_iter.chain(Some(source_frag)).chain(outro_iter) + } + + pub fn edit(&mut self, content: CowStr<'str>, opts: EditOptions) { + if opts.overwrite { + self.intro.clear(); + self.outro.clear(); } + self.keep_in_mappings = opts.store_name; + self.edited_content = Some(content); + } + + pub fn is_edited(&self) -> bool { + self.edited_content.is_some() + } } diff --git a/src/joiner.rs b/src/joiner.rs index 57e1df7..ec4b586 100644 --- a/src/joiner.rs +++ b/src/joiner.rs @@ -1,63 +1,56 @@ -use crate::{MagicString, CowStr}; +use crate::{CowStr, MagicString}; pub struct JoinerOptions { - pub separator: Option, + pub separator: Option, } #[derive(Default)] pub struct Joiner<'s> { - sources: Vec>, - separator: Option, + sources: Vec>, + separator: Option, } impl<'s> Joiner<'s> { - // --- public - pub fn new() -> Self { - Self::default() + // --- public + pub fn new() -> Self { + Self::default() + } + + pub fn with_options(options: JoinerOptions) -> Self { + Self { separator: options.separator, ..Default::default() } + } + + pub fn append(&mut self, source: MagicString<'s>) -> &mut Self { + self.sources.push(source); + self + } + + pub fn append_raw(&mut self, raw: impl Into>) -> &mut Self { + self.sources.push(MagicString::new(raw)); + self + } + + pub fn len(&self) -> usize { + self.fragments().map(|s| s.len()).sum() + } + + pub fn join(&self) -> String { + let mut ret = String::with_capacity(self.len()); + self.fragments().for_each(|frag| { + ret.push_str(frag); + }); + ret + } + + // --- private + + fn fragments(&'s self) -> impl Iterator { + let mut iter = + self.sources.iter().flat_map(|c| self.separator.as_deref().into_iter().chain(c.fragments())); + // Drop the first separator + if self.separator.is_some() { + iter.next(); } - - pub fn with_options(options: JoinerOptions) -> Self { - Self { - separator: options.separator, - ..Default::default() - } - } - - pub fn append(&mut self, source: MagicString<'s>) -> &mut Self { - self.sources.push(source); - self - } - - pub fn append_raw(&mut self, raw: impl Into>) -> &mut Self { - self.sources.push(MagicString::new(raw)); - self - } - - pub fn len(&self) -> usize { - self.fragments().map(|s| s.len()).sum() - } - - pub fn join(&self) -> String { - let mut ret = String::with_capacity(self.len()); - self.fragments().for_each(|frag| { - ret.push_str(frag); - }); - ret - } - - // --- private - - fn fragments(&'s self) -> impl Iterator { - let mut iter = self - .sources - .iter() - .flat_map(|c| self.separator.as_deref().into_iter().chain(c.fragments())); - // Drop the first separator - if self.separator.is_some() { - iter.next(); - } - iter - } - - -} \ No newline at end of file + iter + } +} diff --git a/src/lib.rs b/src/lib.rs index c36f90e..5f54dbd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,22 +1,18 @@ -mod basic_types; mod chunk; mod joiner; -mod locator; mod magic_string; #[cfg(feature = "source_map")] -mod sourcemap_builder; +mod source_map; mod span; +mod type_aliases; -type CowStr<'text> = Cow<'text, str>; - -pub(crate) type TextSize = u32; +type CowStr<'a> = Cow<'a, str>; use std::borrow::Cow; - pub use crate::{ - joiner::{Joiner, JoinerOptions}, - magic_string::{update::UpdateOptions, MagicString, MagicStringOptions, indent::IndentOptions}, + joiner::{Joiner, JoinerOptions}, + magic_string::{indent::IndentOptions, update::UpdateOptions, MagicString, MagicStringOptions}, }; #[cfg(feature = "source_map")] diff --git a/src/locator.rs b/src/locator.rs deleted file mode 100644 index 511db88..0000000 --- a/src/locator.rs +++ /dev/null @@ -1,82 +0,0 @@ -#[derive(Debug)] -pub struct Locator { - /// offsets are calculated based on utf-16 - line_offsets: Box<[usize]>, -} - -impl Locator { - pub fn new(source: &str) -> Self { - let mut line_offsets = vec![]; - let mut line_start_pos = 0; - for line in source.lines() { - line_offsets.push(line_start_pos); - line_start_pos += 1 + line.chars().map(|c| c.len_utf16()).sum::(); - } - Self { - line_offsets: line_offsets.into_boxed_slice(), - } - } - - /// Pass the index based on utf-16 and return the [Location] based on utf-16 - pub fn locate(&self, index: u32) -> Location { - let index = index as usize; - - let mut left_cursor = 0; - let mut right_cursor = self.line_offsets.len(); - while left_cursor < right_cursor { - let mid = (left_cursor + right_cursor) >> 1; - if index < self.line_offsets[mid] { - right_cursor = mid; - } else { - left_cursor = mid + 1; - } - } - let line = left_cursor - 1; - let column = index - self.line_offsets[line]; - Location { - line: line.try_into().unwrap(), - column: column.try_into().unwrap(), - } - } -} - -#[derive(Debug, PartialEq)] -pub struct Location { - pub line: u32, - // columns are calculated based on utf-16 - pub column: u32, -} - -impl Location { - pub fn bump_line(&mut self) { - self.line += 1; - self.column = 0; - } -} - -#[test] -fn basic() { - let source = "string\nwizard"; - let locator = Locator::new(source); - - assert_eq!(locator.line_offsets[0], 0); - assert_eq!(locator.line_offsets[1], 7); - - assert_eq!(locator.locate(0), Location { line: 0, column: 0 }); - assert_eq!(locator.locate(12), Location { line: 1, column: 5 }); - assert_eq!(locator.locate(7), Location { line: 1, column: 0 }); - assert_eq!(locator.locate(1), Location { line: 0, column: 1 }); - assert_eq!(locator.locate(8), Location { line: 1, column: 1 }); -} - -#[test] -fn special_chars() { - let source = "ƟšŸ’£\nšŸ’£ĆŸ"; - let locator = Locator::new(source); - assert_eq!(locator.line_offsets[0], 0); - assert_eq!(locator.line_offsets[1], 4); - - assert_eq!(locator.locate(0), Location { line: 0, column: 0 }); - assert_eq!(locator.locate(4), Location { line: 1, column: 0 }); - assert_eq!(locator.locate(6), Location { line: 1, column: 2 }); -} diff --git a/src/magic_string/append.rs b/src/magic_string/append.rs new file mode 100644 index 0000000..6abac98 --- /dev/null +++ b/src/magic_string/append.rs @@ -0,0 +1,52 @@ +use crate::CowStr; + +use super::MagicString; + +impl<'text> MagicString<'text> { + pub fn append(&mut self, source: impl Into>) -> &mut Self { + self.append_outro(source.into()); + self + } + + /// # Example + ///```rust + /// use string_wizard::MagicString; + /// let mut s = MagicString::new("01234"); + /// s.append_left(2, "a"); + /// s.append_left(2, "b"); + /// assert_eq!(s.to_string(), "01ab234") + ///``` + pub fn append_left(&mut self, text_index: usize, content: impl Into>) -> &mut Self { + match self.by_end_mut(text_index) { + Some(chunk) => { + chunk.append_outro(content.into()); + } + None => self.append_intro(content.into()), + } + self + } + + /// # Example + /// ```rust + /// use string_wizard::MagicString; + /// let mut s = MagicString::new("01234"); + /// s.append_right(2, "A"); + /// s.append_right(2, "B"); + /// s.append_left(2, "a"); + /// s.append_left(2, "b"); + /// assert_eq!(s.to_string(), "01abAB234") + ///``` + pub fn append_right( + &mut self, + text_index: usize, + content: impl Into>, + ) -> &mut Self { + match self.by_start_mut(text_index) { + Some(chunk) => { + chunk.append_intro(content.into()); + } + None => self.append_outro(content.into()), + } + self + } +} diff --git a/src/magic_string/indent.rs b/src/magic_string/indent.rs index 1a707d2..b7caa5d 100644 --- a/src/magic_string/indent.rs +++ b/src/magic_string/indent.rs @@ -1,154 +1,149 @@ use std::borrow::Cow; -use crate::{CowStr, MagicString, TextSize}; +use crate::{CowStr, MagicString}; struct ExcludeSet<'a> { - exclude: &'a [(TextSize, TextSize)], + exclude: &'a [(usize, usize)], } impl<'a> ExcludeSet<'a> { - fn new(exclude: &'a [(TextSize, TextSize)]) -> Self { - Self { exclude } - } + fn new(exclude: &'a [(usize, usize)]) -> Self { + Self { exclude } + } - fn contains(&self, index: TextSize) -> bool { - self.exclude.iter().any(|s| s.0 <= index && index < s.1) - } + fn contains(&self, index: usize) -> bool { + self.exclude.iter().any(|s| s.0 <= index && index < s.1) + } } pub fn guess_indentor(source: &str) -> Option { - let mut tabbed_count = 0; - let mut spaced_line = vec![]; - for line in source.lines() { - if line.starts_with('\t') { - tabbed_count += 1; - } else if line.starts_with(" ") { - spaced_line.push(line); - } - } - - if tabbed_count == 0 && spaced_line.is_empty() { - return None; - } - - if tabbed_count >= spaced_line.len() { - return Some("\t".to_string()); - } - - let min_space_count = spaced_line - .iter() - .map(|line| line.chars().take_while(|c| *c == ' ').count()) - .min() - .unwrap_or(0); - - let mut indent_str = String::with_capacity(min_space_count); - for _ in 0..min_space_count { - indent_str.push(' '); + let mut tabbed_count = 0; + let mut spaced_line = vec![]; + for line in source.lines() { + if line.starts_with('\t') { + tabbed_count += 1; + } else if line.starts_with(" ") { + spaced_line.push(line); } - Some(indent_str) + } + + if tabbed_count == 0 && spaced_line.is_empty() { + return None; + } + + if tabbed_count >= spaced_line.len() { + return Some("\t".to_string()); + } + + let min_space_count = spaced_line + .iter() + .map(|line| line.chars().take_while(|c| *c == ' ').count()) + .min() + .unwrap_or(0); + + let mut indent_str = String::with_capacity(min_space_count); + for _ in 0..min_space_count { + indent_str.push(' '); + } + Some(indent_str) } #[derive(Debug, Default)] pub struct IndentOptions<'a, 'b> { - /// MagicString will guess the `indentor`` from lines of the source if passed `None`. - pub indentor: Option<&'a str>, + /// MagicString will guess the `indentor` from lines of the source if passed `None`. + pub indentor: Option<&'a str>, - /// The reason I use `[u32; 2]` instead of `(u32, u32)` to represent a range of text is that - /// I want to emphasize that the `[u32; 2]` is the closed interval, which means both the start - /// and the end are included in the range. - pub exclude: &'b [(TextSize, TextSize)], + /// The reason I use `[u32; 2]` instead of `(u32, u32)` to represent a range of text is that + /// I want to emphasize that the `[u32; 2]` is the closed interval, which means both the start + /// and the end are included in the range. + pub exclude: &'b [(usize, usize)], } impl<'text> MagicString<'text> { - pub fn guessed_indentor(&mut self) -> &str { - let guessed_indentor = self - .guessed_indentor - .get_or_init(|| guess_indentor(&self.source).unwrap_or_else(|| "\t".to_string())); - guessed_indentor + fn guessed_indentor(&mut self) -> &str { + let guessed_indentor = self + .guessed_indentor + .get_or_init(|| guess_indentor(&self.source).unwrap_or_else(|| "\t".to_string())); + guessed_indentor + } + + pub fn indent(&mut self) -> &mut Self { + self.indent_with(IndentOptions { indentor: None, ..Default::default() }) + } + + pub fn indent_with(&mut self, opts: IndentOptions) -> &mut Self { + if opts.indentor.map_or(false, |s| s.is_empty()) { + return self; } - - pub fn indent(&mut self) -> &mut Self { - self.indent_with(IndentOptions { - indentor: None, - ..Default::default() - }) + struct IndentReplacer { + should_indent_next_char: bool, + indentor: String, } - pub fn indent_with(&mut self, opts: IndentOptions) -> &mut Self { - if opts.indentor.map_or(false, |s| s.is_empty()) { - return self; - } - struct IndentReplacer { - should_indent_next_char: bool, - indentor: String, + fn indent_frag<'text>(frag: &mut CowStr<'text>, indent_replacer: &mut IndentReplacer) { + let mut indented = String::new(); + for char in frag.chars() { + if char == '\n' { + indent_replacer.should_indent_next_char = true; + } else if char != '\r' && indent_replacer.should_indent_next_char { + indent_replacer.should_indent_next_char = false; + indented.push_str(&indent_replacer.indentor); } + indented.push(char); + } + *frag = Cow::Owned(indented); + } - fn indent_frag<'text>(frag: &mut CowStr<'text>, indent_replacer: &mut IndentReplacer) { - let mut indented = String::new(); - for char in frag.chars() { - if char == '\n' { - indent_replacer.should_indent_next_char = true; - } else if char != '\r' && indent_replacer.should_indent_next_char { - indent_replacer.should_indent_next_char = false; - indented.push_str(&indent_replacer.indentor); - } - indented.push(char); - } - *frag = Cow::Owned(indented); - } + let indentor = opts.indentor.unwrap_or_else(|| self.guessed_indentor()); - let indentor = opts.indentor.unwrap_or_else(|| self.guessed_indentor()); + let mut indent_replacer = + IndentReplacer { should_indent_next_char: true, indentor: indentor.to_string() }; - let mut indent_replacer = IndentReplacer { - should_indent_next_char: true, - indentor: indentor.to_string(), - }; + for intro_frag in self.intro.iter_mut() { + indent_frag(intro_frag, &mut indent_replacer) + } - for intro_frag in self.intro.iter_mut() { - indent_frag(intro_frag, &mut indent_replacer) + let exclude_set = ExcludeSet::new(opts.exclude); + + let mut next_chunk_id = Some(self.first_chunk_idx); + let mut char_index = 0; + while let Some(chunk_idx) = next_chunk_id { + // Make sure the `next_chunk_id` is updated before we split the chunk. Otherwise, we + // might process the same chunk twice. + next_chunk_id = self.chunks[chunk_idx].next; + if let Some(edited_content) = self.chunks[chunk_idx].edited_content.as_mut() { + if !exclude_set.contains(char_index) { + indent_frag(edited_content, &mut indent_replacer); } - - let exclude_set = ExcludeSet::new(opts.exclude); - - let mut next_chunk_id = Some(self.first_chunk_idx); - let mut char_index = 0u32; - while let Some(chunk_idx) = next_chunk_id { - // Make sure the `next_chunk_id` is updated before we split the chunk. Otherwise, we - // might process the same chunk twice. - next_chunk_id = self.chunks[chunk_idx].next; - if let Some(edited_content) = self.chunks[chunk_idx].edited_content.as_mut() { - if !exclude_set.contains(char_index) { - indent_frag(edited_content, &mut indent_replacer); - } - } else { - let chunk = &self.chunks[chunk_idx]; - let mut line_starts = vec![]; - char_index = chunk.start(); - let chunk_end = chunk.end(); - for char in chunk.span.text(&self.source).chars() { - debug_assert!(self.source.is_char_boundary(char_index as usize)); - if !exclude_set.contains(char_index) { - if char == '\n' { - indent_replacer.should_indent_next_char = true; - } else if char != '\r' && indent_replacer.should_indent_next_char { - indent_replacer.should_indent_next_char = false; - debug_assert!(!line_starts.contains(&char_index)); - line_starts.push(char_index); - } - } - char_index += char.len_utf8() as u32; - } - for line_start in line_starts { - self.prepend_right(line_start, indent_replacer.indentor.clone()); - } - char_index = chunk_end; + } else { + let chunk = &self.chunks[chunk_idx]; + let mut line_starts = vec![]; + char_index = chunk.start(); + let chunk_end = chunk.end(); + for char in chunk.span.text(&self.source).chars() { + debug_assert!(self.source.is_char_boundary(char_index as usize)); + if !exclude_set.contains(char_index) { + if char == '\n' { + indent_replacer.should_indent_next_char = true; + } else if char != '\r' && indent_replacer.should_indent_next_char { + indent_replacer.should_indent_next_char = false; + debug_assert!(!line_starts.contains(&char_index)); + line_starts.push(char_index); } + } + char_index += char.len_utf8(); } - - for frag in self.outro.iter_mut() { - indent_frag(frag, &mut indent_replacer) + for line_start in line_starts { + self.prepend_right(line_start, indent_replacer.indentor.clone()); } + char_index = chunk_end; + } + } - self + for frag in self.outro.iter_mut() { + indent_frag(frag, &mut indent_replacer) } + + self + } } diff --git a/src/magic_string/mod.rs b/src/magic_string/mod.rs index 52c201c..f127cbd 100644 --- a/src/magic_string/mod.rs +++ b/src/magic_string/mod.rs @@ -1,305 +1,213 @@ +pub mod append; pub mod indent; -pub mod mutation; +pub mod movement; +pub mod prepend; #[cfg(feature = "source_map")] pub mod source_map; pub mod update; -use std::collections::VecDeque; +use std::{collections::VecDeque, sync::OnceLock}; -use once_cell::sync::OnceCell; use rustc_hash::FxHashMap; use crate::{ - basic_types::AssertIntoU32, - chunk::{Chunk, ChunkIdx, ChunkVec}, - span::Span, - CowStr, TextSize, + chunk::{Chunk, ChunkIdx}, + span::Span, + type_aliases::IndexChunks, + CowStr, }; #[derive(Debug, Default)] pub struct MagicStringOptions { - pub filename: Option, + pub filename: Option, } #[derive(Debug, Clone)] pub struct MagicString<'s> { - pub filename: Option, - intro: VecDeque>, - outro: VecDeque>, - source: CowStr<'s>, - source_len: TextSize, - chunks: ChunkVec<'s>, - first_chunk_idx: ChunkIdx, - last_chunk_idx: ChunkIdx, - chunk_by_start: FxHashMap, - chunk_by_end: FxHashMap, - guessed_indentor: OnceCell, - - // This is used to speed up the search for the chunk that contains a given index. - last_searched_chunk_idx: ChunkIdx, + pub filename: Option, + intro: VecDeque>, + outro: VecDeque>, + source: CowStr<'s>, + chunks: IndexChunks<'s>, + first_chunk_idx: ChunkIdx, + last_chunk_idx: ChunkIdx, + chunk_by_start: FxHashMap, + chunk_by_end: FxHashMap, + guessed_indentor: OnceLock, + + // This is used to speed up the search for the chunk that contains a given index. + last_searched_chunk_idx: ChunkIdx, } impl<'text> MagicString<'text> { - // --- public - - pub fn new(source: impl Into>) -> Self { - Self::with_options(source, Default::default()) - } - - pub fn with_options(source: impl Into>, options: MagicStringOptions) -> Self { - let source = source.into(); - let source_len = u32::try_from(source.len()).unwrap(); - let initial_chunk = Chunk::new(Span(0, source_len)); - let mut chunks = ChunkVec::with_capacity(1); - let initial_chunk_idx = chunks.push(initial_chunk); - let mut magic_string = Self { - intro: Default::default(), - outro: Default::default(), - source, - source_len, - first_chunk_idx: initial_chunk_idx, - last_chunk_idx: initial_chunk_idx, - chunks, - chunk_by_start: Default::default(), - chunk_by_end: Default::default(), - // setup options - filename: options.filename, - guessed_indentor: OnceCell::default(), - last_searched_chunk_idx: initial_chunk_idx, - }; - - magic_string.chunk_by_start.insert(0, initial_chunk_idx); - magic_string - .chunk_by_end - .insert(source_len, initial_chunk_idx); - - magic_string - } - - pub fn append(&mut self, source: impl Into>) -> &mut Self { - self.append_outro(source.into()); - self - } - - /// # Example - ///```rust - /// use string_wizard::MagicString; - /// let mut s = MagicString::new("01234"); - /// s.append_left(2, "a"); - /// s.append_left(2, "b"); - /// assert_eq!(s.to_string(), "01ab234") - ///``` - pub fn append_left( - &mut self, - text_index: impl AssertIntoU32, - content: impl Into>, - ) -> &mut Self { - match self.by_end_mut(text_index.assert_into_u32()) { - Some(chunk) => { - chunk.append_outro(content.into()); - } - None => self.append_intro(content.into()), - } - self - } - - /// # Example - /// ```rust - /// use string_wizard::MagicString; - /// let mut s = MagicString::new("01234"); - /// s.append_right(2, "A"); - /// s.append_right(2, "B"); - /// s.append_left(2, "a"); - /// s.append_left(2, "b"); - /// assert_eq!(s.to_string(), "01abAB234") - ///``` - pub fn append_right( - &mut self, - text_index: impl AssertIntoU32, - content: impl Into>, - ) -> &mut Self { - match self.by_start_mut(text_index.assert_into_u32()) { - Some(chunk) => { - chunk.append_intro(content.into()); - } - None => self.append_outro(content.into()), - } - self - } - - pub fn prepend(&mut self, source: impl Into>) -> &mut Self { - self.prepend_intro(source.into()); - self - } - - pub fn prepend_left( - &mut self, - text_index: impl AssertIntoU32, - content: impl Into>, - ) -> &mut Self { - match self.by_end_mut(text_index.assert_into_u32()) { - Some(chunk) => chunk.prepend_outro(content.into()), - None => self.prepend_intro(content.into()), - } - self - } - - pub fn prepend_right( - &mut self, - text_index: impl AssertIntoU32, - content: impl Into>, - ) -> &mut Self { - match self.by_start_mut(text_index.assert_into_u32()) { - Some(chunk) => { - chunk.prepend_intro(content.into()); - } - None => self.prepend_outro(content.into()), - } - self - } - - pub fn len(&self) -> usize { - self.fragments().map(|f| f.len()).sum() - } - - pub fn to_string(&self) -> String { - let size_hint = self.len(); - let mut ret = String::with_capacity(size_hint); - self.fragments().for_each(|f| ret.push_str(f)); - ret - } - - // --- private - - fn prepend_intro(&mut self, content: impl Into>) { - self.intro.push_front(content.into()); - } - - fn append_outro(&mut self, content: impl Into>) { - self.outro.push_back(content.into()); - } - - fn prepend_outro(&mut self, content: impl Into>) { - self.outro.push_front(content.into()); - } - - fn append_intro(&mut self, content: impl Into>) { - self.intro.push_back(content.into()); - } - - fn iter_chunks(&self) -> impl Iterator { - ChunkIter { - next: Some(self.first_chunk_idx), - chunks: &self.chunks, - } - } - - pub(crate) fn fragments(&'text self) -> impl Iterator { - let intro = self.intro.iter().map(|s| s.as_ref()); - let outro = self.outro.iter().map(|s| s.as_ref()); - let chunks = self.iter_chunks().flat_map(|c| c.fragments(&self.source)); - intro.chain(chunks).chain(outro) - } - - /// For input - /// "abcdefg" - /// 0123456 - /// - /// Chunk{span: (0, 7)} => "abcdefg" - /// - /// split_at(3) would create - /// - /// Chunk{span: (0, 3)} => "abc" - /// Chunk{span: (3, 7)} => "defg" - fn split_at(&mut self, at_index: u32) { - if at_index == 0 || at_index >= self.source_len || self.chunk_by_end.contains_key(&at_index) - { - return; - } - - let (mut candidate, mut candidate_idx, search_right) = { - let last_searched_chunk = &self.chunks[self.last_searched_chunk_idx]; - let search_right = at_index > last_searched_chunk.end(); - - (last_searched_chunk, self.last_searched_chunk_idx, search_right) - }; - - - while !candidate.contains(at_index) { - let next_idx = if search_right { - self.chunk_by_start[&candidate.end()] - } else { - self.chunk_by_end[&candidate.start()] - }; - candidate = &self.chunks[next_idx]; - candidate_idx = next_idx; - } - - let second_half_chunk = self.chunks[candidate_idx].split(at_index); - let second_half_span = second_half_chunk.span; - let second_half_idx = self.chunks.push(second_half_chunk); - let first_half_idx = candidate_idx; - - // Update the last searched chunk - self.last_searched_chunk_idx = first_half_idx; - - // Update the chunk_by_start/end maps - self.chunk_by_end.insert(at_index, first_half_idx); - self.chunk_by_start.insert(at_index, second_half_idx); - self.chunk_by_end - .insert(second_half_span.end(), second_half_idx); - - // Make sure the new chunk and the old chunk have correct next/prev pointers - self.chunks[second_half_idx].next = self.chunks[first_half_idx].next; - if let Some(second_half_next_idx) = self.chunks[second_half_idx].next { - self.chunks[second_half_next_idx].prev = Some(second_half_idx); - } - self.chunks[second_half_idx].prev = Some(first_half_idx); - self.chunks[first_half_idx].next = Some(second_half_idx); - if first_half_idx == self.last_chunk_idx { - self.last_chunk_idx = second_half_idx - } - } - - fn by_start_mut(&mut self, text_index: impl AssertIntoU32) -> Option<&mut Chunk<'text>> { - let text_index = text_index.assert_into_u32(); - if text_index == self.source_len { - None - } else { - self.split_at(text_index); - // TODO: safety: using `unwrap_unchecked` is fine. - let idx = self.chunk_by_start.get(&text_index).unwrap(); - Some(&mut self.chunks[*idx]) - } - } - - fn by_end_mut(&mut self, text_index: TextSize) -> Option<&mut Chunk<'text>> { - let text_index = text_index.assert_into_u32(); - if text_index == 0 { - None - } else { - self.split_at(text_index); - // TODO: safety: using `unwrap_unchecked` is fine. - let idx = self.chunk_by_end.get(&text_index).unwrap(); - Some(&mut self.chunks[*idx]) - } - } + // --- public + + pub fn new(source: impl Into>) -> Self { + Self::with_options(source, Default::default()) + } + + pub fn with_options(source: impl Into>, options: MagicStringOptions) -> Self { + let source: CowStr = source.into(); + let source_len = source.len(); + let initial_chunk = Chunk::new(Span(0, source_len)); + let mut chunks = IndexChunks::with_capacity(1); + let initial_chunk_idx = chunks.push(initial_chunk); + let mut magic_string = Self { + intro: Default::default(), + outro: Default::default(), + source, + first_chunk_idx: initial_chunk_idx, + last_chunk_idx: initial_chunk_idx, + chunks, + chunk_by_start: Default::default(), + chunk_by_end: Default::default(), + filename: options.filename, + guessed_indentor: OnceLock::default(), + last_searched_chunk_idx: initial_chunk_idx, + }; + + magic_string.chunk_by_start.insert(0, initial_chunk_idx); + magic_string.chunk_by_end.insert(source_len, initial_chunk_idx); + + magic_string + } + + pub fn len(&self) -> usize { + self.fragments().map(|f| f.len()).sum() + } + + pub fn to_string(&self) -> String { + let size_hint = self.len(); + let mut ret = String::with_capacity(size_hint); + self.fragments().for_each(|f| ret.push_str(f)); + ret + } + + // --- private + + fn prepend_intro(&mut self, content: impl Into>) { + self.intro.push_front(content.into()); + } + + fn append_outro(&mut self, content: impl Into>) { + self.outro.push_back(content.into()); + } + + fn prepend_outro(&mut self, content: impl Into>) { + self.outro.push_front(content.into()); + } + + fn append_intro(&mut self, content: impl Into>) { + self.intro.push_back(content.into()); + } + + fn iter_chunks(&self) -> impl Iterator { + IterChunks { next: Some(self.first_chunk_idx), chunks: &self.chunks } + } + + pub(crate) fn fragments(&'text self) -> impl Iterator { + let intro = self.intro.iter().map(|s| s.as_ref()); + let outro = self.outro.iter().map(|s| s.as_ref()); + let chunks = self.iter_chunks().flat_map(|c| c.fragments(&self.source)); + intro.chain(chunks).chain(outro) + } + + /// For input + /// "abcdefg" + /// 0123456 + /// + /// Chunk{span: (0, 7)} => "abcdefg" + /// + /// split_at(3) would create + /// + /// Chunk{span: (0, 3)} => "abc" + /// Chunk{span: (3, 7)} => "defg" + fn split_at(&mut self, at_index: usize) { + if at_index == 0 || at_index >= self.source.len() || self.chunk_by_end.contains_key(&at_index) { + return; + } + + let (mut candidate, mut candidate_idx, search_right) = { + let last_searched_chunk = &self.chunks[self.last_searched_chunk_idx]; + let search_right = at_index > last_searched_chunk.end(); + + (last_searched_chunk, self.last_searched_chunk_idx, search_right) + }; + + while !candidate.contains(at_index) { + let next_idx = if search_right { + self.chunk_by_start[&candidate.end()] + } else { + self.chunk_by_end[&candidate.start()] + }; + candidate = &self.chunks[next_idx]; + candidate_idx = next_idx; + } + + let second_half_chunk = self.chunks[candidate_idx].split(at_index); + let second_half_span = second_half_chunk.span; + let second_half_idx = self.chunks.push(second_half_chunk); + let first_half_idx = candidate_idx; + + // Update the last searched chunk + self.last_searched_chunk_idx = first_half_idx; + + // Update the chunk_by_start/end maps + self.chunk_by_end.insert(at_index, first_half_idx); + self.chunk_by_start.insert(at_index, second_half_idx); + self.chunk_by_end.insert(second_half_span.end(), second_half_idx); + + // Make sure the new chunk and the old chunk have correct next/prev pointers + self.chunks[second_half_idx].next = self.chunks[first_half_idx].next; + if let Some(second_half_next_idx) = self.chunks[second_half_idx].next { + self.chunks[second_half_next_idx].prev = Some(second_half_idx); + } + self.chunks[second_half_idx].prev = Some(first_half_idx); + self.chunks[first_half_idx].next = Some(second_half_idx); + if first_half_idx == self.last_chunk_idx { + self.last_chunk_idx = second_half_idx + } + } + + fn by_start_mut(&mut self, text_index: usize) -> Option<&mut Chunk<'text>> { + if text_index == self.source.len() { + None + } else { + self.split_at(text_index); + // TODO: safety: using `unwrap_unchecked` is fine. + let idx = self.chunk_by_start.get(&text_index).unwrap(); + Some(&mut self.chunks[*idx]) + } + } + + fn by_end_mut(&mut self, text_index: usize) -> Option<&mut Chunk<'text>> { + if text_index == 0 { + None + } else { + self.split_at(text_index); + // TODO: safety: using `unwrap_unchecked` is fine. + let idx = self.chunk_by_end.get(&text_index).unwrap(); + Some(&mut self.chunks[*idx]) + } + } } -struct ChunkIter<'a> { - next: Option, - chunks: &'a ChunkVec<'a>, +struct IterChunks<'a> { + next: Option, + chunks: &'a IndexChunks<'a>, } -impl<'a> Iterator for ChunkIter<'a> { - type Item = &'a Chunk<'a>; - - fn next(&mut self) -> Option { - self.next.take().map(|next| { - let chunk = &self.chunks[next]; - self.next = chunk.next; +impl<'a> Iterator for IterChunks<'a> { + type Item = &'a Chunk<'a>; - chunk - }) + fn next(&mut self) -> Option { + match self.next { + None => None, + Some(idx) => { + let chunk = &self.chunks[idx]; + self.next = chunk.next; + Some(chunk) + } } + } } diff --git a/src/magic_string/movement.rs b/src/magic_string/movement.rs new file mode 100644 index 0000000..fd2496e --- /dev/null +++ b/src/magic_string/movement.rs @@ -0,0 +1,86 @@ +use crate::MagicString; + +use super::update::UpdateOptions; + +impl<'text> MagicString<'text> { + pub fn remove(&mut self, start: usize, end: usize) -> &mut Self { + self.inner_update_with( + start, + end, + "".into(), + UpdateOptions { keep_original: false, overwrite: true }, + false, + ); + + self + } + + /// Moves the characters from start and end to index. Returns this. + // `move` is reserved keyword in rust, so we use `relocate` instead. + pub fn relocate(&mut self, start: usize, end: usize, to: usize) -> &mut Self { + if to >= start && to <= end { + panic!("Cannot relocate a selection inside itself") + } + + self.split_at(start); + self.split_at(end); + self.split_at(to); + + let first_idx = self.chunk_by_start[&start]; + let last_idx = self.chunk_by_end[&end]; + + let old_left_idx = self.chunks[first_idx].prev; + let old_right_idx = self.chunks[last_idx].next; + + let new_right_idx = self.chunk_by_start.get(&to).copied(); + + // `new_right_idx` is `None` means that the `to` index is at the end of the string. + // Moving chunks which contain the last chunk to the end is meaningless. + if new_right_idx.is_none() && last_idx == self.last_chunk_idx { + return self; + } + + let new_left_idx = new_right_idx + .map(|idx| self.chunks[idx].prev) + // If the `to` index is at the end of the string, then the `new_right_idx` will be `None`. + // In this case, we want to use the last chunk as the left chunk to connect the relocated chunk. + .unwrap_or(Some(self.last_chunk_idx)); + + // Adjust next/prev pointers, this remove the [start, end] range from the old position + if let Some(old_left_idx) = old_left_idx { + self.chunks[old_left_idx].next = old_right_idx; + } + if let Some(old_right_idx) = old_right_idx { + self.chunks[old_right_idx].prev = old_left_idx; + } + + if let Some(new_left_idx) = new_left_idx { + self.chunks[new_left_idx].next = Some(first_idx); + } + if let Some(new_right_idx) = new_right_idx { + self.chunks[new_right_idx].prev = Some(last_idx); + } + + if self.chunks[first_idx].prev.is_none() { + // If the `first_idx` is the first chunk, then we need to update the `first_chunk_idx`. + self.first_chunk_idx = self.chunks[last_idx].next.unwrap(); + } + if self.chunks[last_idx].next.is_none() { + // If the `last_idx` is the last chunk, then we need to update the `last_chunk_idx`. + self.last_chunk_idx = self.chunks[first_idx].prev.unwrap(); + self.chunks[last_idx].next = None; + } + + if new_left_idx.is_none() { + self.first_chunk_idx = first_idx; + } + if new_right_idx.is_none() { + self.last_chunk_idx = last_idx; + } + + self.chunks[first_idx].prev = new_left_idx; + self.chunks[last_idx].next = new_right_idx; + + self + } +} diff --git a/src/magic_string/mutation.rs b/src/magic_string/mutation.rs deleted file mode 100644 index 9600429..0000000 --- a/src/magic_string/mutation.rs +++ /dev/null @@ -1,97 +0,0 @@ -use crate::{basic_types::AssertIntoU32, MagicString}; - -use super::update::UpdateOptions; - -impl<'text> MagicString<'text> { - pub fn remove(&mut self, start: impl AssertIntoU32, end: impl AssertIntoU32) -> &mut Self { - self.update_with_inner( - start.assert_into_u32(), - end.assert_into_u32(), - "".into(), - UpdateOptions { - keep_original: false, - overwrite: true, - }, - false, - ); - - self - } - - /// Moves the characters from start and end to index. Returns this. - // `move` is reserved keyword in rust, so we use `relocate` instead. - pub fn relocate( - &mut self, - start: impl AssertIntoU32, - end: impl AssertIntoU32, - to: impl AssertIntoU32, - ) -> &mut Self { - let start = start.assert_into_u32(); - let end = end.assert_into_u32(); - let to = to.assert_into_u32(); - if to >= start && to <= end { - panic!("Cannot relocate a selection inside itself") - } - - self.split_at(start); - self.split_at(end); - self.split_at(to); - - let first_idx = self.chunk_by_start[&start]; - let last_idx = self.chunk_by_end[&end]; - - let old_left_idx = self.chunks[first_idx].prev; - let old_right_idx = self.chunks[last_idx].next; - - let new_right_idx = self.chunk_by_start.get(&to).copied(); - - // `new_right_idx` is `None` means that the `to` index is at the end of the string. - // Moving chunks which contain the last chunk to the end is meaningless. - if new_right_idx.is_none() && last_idx == self.last_chunk_idx { - return self; - } - - let new_left_idx = new_right_idx - .map(|idx| self.chunks[idx].prev) - // If the `to` index is at the end of the string, then the `new_right_idx` will be `None`. - // In this case, we want to use the last chunk as the left chunk to connect the relocated chunk. - .unwrap_or(Some(self.last_chunk_idx)); - - // Adjust next/prev pointers, this remove the [start, end] range from the old position - if let Some(old_left_idx) = old_left_idx { - self.chunks[old_left_idx].next = old_right_idx; - } - if let Some(old_right_idx) = old_right_idx { - self.chunks[old_right_idx].prev = old_left_idx; - } - - if let Some(new_left_idx) = new_left_idx { - self.chunks[new_left_idx].next = Some(first_idx); - } - if let Some(new_right_idx) = new_right_idx { - self.chunks[new_right_idx].prev = Some(last_idx); - } - - if self.chunks[first_idx].prev.is_none() { - // If the `first_idx` is the first chunk, then we need to update the `first_chunk_idx`. - self.first_chunk_idx = self.chunks[last_idx].next.unwrap(); - } - if self.chunks[last_idx].next.is_none() { - // If the `last_idx` is the last chunk, then we need to update the `last_chunk_idx`. - self.last_chunk_idx = self.chunks[first_idx].prev.unwrap(); - self.chunks[last_idx].next = None; - } - - if new_left_idx.is_none() { - self.first_chunk_idx = first_idx; - } - if new_right_idx.is_none() { - self.last_chunk_idx = last_idx; - } - - self.chunks[first_idx].prev = new_left_idx; - self.chunks[last_idx].next = new_right_idx; - - self - } -} diff --git a/src/magic_string/prepend.rs b/src/magic_string/prepend.rs new file mode 100644 index 0000000..ab36e36 --- /dev/null +++ b/src/magic_string/prepend.rs @@ -0,0 +1,36 @@ +use crate::CowStr; + +use super::MagicString; + +impl<'text> MagicString<'text> { + pub fn prepend(&mut self, source: impl Into>) -> &mut Self { + self.prepend_intro(source.into()); + self + } + + pub fn prepend_left( + &mut self, + text_index: usize, + content: impl Into>, + ) -> &mut Self { + match self.by_end_mut(text_index) { + Some(chunk) => chunk.prepend_outro(content.into()), + None => self.prepend_intro(content.into()), + } + self + } + + pub fn prepend_right( + &mut self, + text_index: usize, + content: impl Into>, + ) -> &mut Self { + match self.by_start_mut(text_index) { + Some(chunk) => { + chunk.prepend_intro(content.into()); + } + None => self.prepend_outro(content.into()), + } + self + } +} diff --git a/src/magic_string/source_map.rs b/src/magic_string/source_map.rs index 399cb1b..6f48925 100644 --- a/src/magic_string/source_map.rs +++ b/src/magic_string/source_map.rs @@ -1,54 +1,53 @@ use std::sync::Arc; -use crate::{locator::Locator, sourcemap_builder::SourcemapBuilder, MagicString}; +use crate::{ + source_map::{locator::Locator, sourcemap_builder::SourcemapBuilder}, + MagicString, +}; #[derive(Debug)] pub struct SourceMapOptions { - pub include_content: bool, - pub source: Arc, - pub hires: bool + pub include_content: bool, + pub source: Arc, + pub hires: bool, } impl Default for SourceMapOptions { - fn default() -> Self { - Self { - include_content: false, - source: "".into(), - hires: false - } - } + fn default() -> Self { + Self { include_content: false, source: "".into(), hires: false } + } } impl<'s> MagicString<'s> { - pub fn source_map(&self, opts: SourceMapOptions) -> oxc_sourcemap::SourceMap { - let mut source_builder = SourcemapBuilder::new(opts.hires); + pub fn source_map(&self, opts: SourceMapOptions) -> oxc_sourcemap::SourceMap { + let mut source_builder = SourcemapBuilder::new(opts.hires); - source_builder.set_source_and_content(&opts.source, &self.source); + source_builder.set_source_and_content(&opts.source, &self.source); - let locator = Locator::new(&self.source); + let locator = Locator::new(&self.source); - self.intro.iter().for_each(|frag| { - source_builder.advance(frag); - }); + self.intro.iter().for_each(|frag| { + source_builder.advance(frag); + }); - self.iter_chunks().for_each(|chunk| { - chunk.intro.iter().for_each(|frag| { - source_builder.advance(frag); - }); + self.iter_chunks().for_each(|chunk| { + chunk.intro.iter().for_each(|frag| { + source_builder.advance(frag); + }); - let name = if chunk.keep_in_mappings && chunk.is_edited() { - Some(chunk.span.text(&self.source)) - } else { - None - }; + let name = if chunk.keep_in_mappings && chunk.is_edited() { + Some(chunk.span.text(&self.source)) + } else { + None + }; - source_builder.add_chunk(chunk, &locator, &self.source, name); + source_builder.add_chunk(chunk, &locator, &self.source, name); - chunk.outro.iter().for_each(|frag| { - source_builder.advance(frag); - }); - }); + chunk.outro.iter().for_each(|frag| { + source_builder.advance(frag); + }); + }); - source_builder.into_source_map() - } + source_builder.into_source_map() + } } diff --git a/src/magic_string/update.rs b/src/magic_string/update.rs index 3f8ad67..42128d7 100644 --- a/src/magic_string/update.rs +++ b/src/magic_string/update.rs @@ -1,86 +1,73 @@ -use crate::{basic_types::AssertIntoU32, chunk::EditOptions, CowStr, MagicString}; +use crate::{chunk::EditOptions, CowStr, MagicString}; #[derive(Debug, Default, Clone)] pub struct UpdateOptions { - /// `true` will store the original content in the `name` field of the generated sourcemap. - pub keep_original: bool, + /// `true` will store the original content in the `name` field of the generated sourcemap. + pub keep_original: bool, - /// `true` will clear the `intro` and `outro` for the corresponding range. - pub overwrite: bool, + /// `true` will clear the `intro` and `outro` for the corresponding range. + pub overwrite: bool, } impl<'text> MagicString<'text> { - /// A shorthand for `update_with(start, end, content, Default::default())`; - pub fn update( - &mut self, - start: impl AssertIntoU32, - end: impl AssertIntoU32, - content: impl Into>, - ) -> &mut Self { - self.update_with(start, end, content, Default::default()) - } + /// A shorthand for `update_with(start, end, content, Default::default())`; + pub fn update( + &mut self, + start: usize, + end: usize, + content: impl Into>, + ) -> &mut Self { + self.update_with(start, end, content, Default::default()) + } - pub fn update_with( - &mut self, - start: impl AssertIntoU32, - end: impl AssertIntoU32, - content: impl Into>, - opts: UpdateOptions, - ) -> &mut Self { - self.update_with_inner( - start.assert_into_u32(), - end.assert_into_u32(), - content.into(), - opts, - true, - ); - self - } + pub fn update_with( + &mut self, + start: usize, + end: usize, + content: impl Into>, + opts: UpdateOptions, + ) -> &mut Self { + self.inner_update_with(start, end, content.into(), opts, true); + self + } - // --- private + // --- private - pub(super) fn update_with_inner( - &mut self, - start: u32, - end: u32, - content: CowStr<'text>, - opts: UpdateOptions, - panic_if_start_equal_end: bool, - ) -> &mut Self { - let start = start as u32; - let end = end as u32; - if panic_if_start_equal_end && start == end { - panic!( - "Cannot overwrite a zero-length range ā€“ use append_left or prepend_right instead" - ) - } - assert!(start < end); - self.split_at(start); - self.split_at(end); + pub(super) fn inner_update_with( + &mut self, + start: usize, + end: usize, + content: CowStr<'text>, + opts: UpdateOptions, + panic_if_start_equal_end: bool, + ) -> &mut Self { + if panic_if_start_equal_end && start == end { + panic!("Cannot overwrite a zero-length range ā€“ use append_left or prepend_right instead") + } + assert!(start < end); + self.split_at(start); + self.split_at(end); - let start_idx = self.chunk_by_start.get(&start).copied().unwrap(); - let end_idx = self.chunk_by_end.get(&end).copied().unwrap(); + let start_idx = self.chunk_by_start.get(&start).copied().unwrap(); + let end_idx = self.chunk_by_end.get(&end).copied().unwrap(); - let start_chunk = &mut self.chunks[start_idx]; - start_chunk.edit( - content.into(), - EditOptions { - overwrite: opts.overwrite, - store_name: opts.keep_original, - }, - ); + let start_chunk = &mut self.chunks[start_idx]; + start_chunk.edit( + content.into(), + EditOptions { overwrite: opts.overwrite, store_name: opts.keep_original }, + ); - let mut rest_chunk_idx = if start_idx != end_idx { - start_chunk.next.unwrap() - } else { - return self; - }; + let mut rest_chunk_idx = if start_idx != end_idx { + start_chunk.next.unwrap() + } else { + return self; + }; - while rest_chunk_idx != end_idx { - let rest_chunk = &mut self.chunks[rest_chunk_idx]; - rest_chunk.edit("".into(), Default::default()); - rest_chunk_idx = rest_chunk.next.unwrap(); - } - self + while rest_chunk_idx != end_idx { + let rest_chunk = &mut self.chunks[rest_chunk_idx]; + rest_chunk.edit("".into(), Default::default()); + rest_chunk_idx = rest_chunk.next.unwrap(); } + self + } } diff --git a/src/source_map/locator.rs b/src/source_map/locator.rs new file mode 100644 index 0000000..7288cf0 --- /dev/null +++ b/src/source_map/locator.rs @@ -0,0 +1,84 @@ +#[derive(Debug)] +pub struct Locator { + /// offsets are calculated based on utf-16 + line_offsets: Box<[usize]>, +} + +impl Locator { + pub fn new(source: &str) -> Self { + let mut line_offsets = vec![]; + let mut line_start_pos = 0; + for line in source.split('\n') { + line_offsets.push(line_start_pos); + line_start_pos += 1 + line.chars().map(|c| c.len_utf16()).sum::(); + } + Self { line_offsets: line_offsets.into_boxed_slice() } + } + + /// Pass the index based on utf-16 and return the [Location] based on utf-16 + pub fn locate(&self, index: usize) -> Location { + let index = index as usize; + + let mut left_cursor = 0; + let mut right_cursor = self.line_offsets.len(); + while left_cursor < right_cursor { + let mid = (left_cursor + right_cursor) >> 1; + if index < self.line_offsets[mid] { + right_cursor = mid; + } else { + left_cursor = mid + 1; + } + } + let line = left_cursor - 1; + let column = index - self.line_offsets[line]; + Location { line: line.try_into().unwrap(), column: column.try_into().unwrap() } + } +} + +#[derive(Debug, PartialEq)] +pub struct Location { + pub line: usize, + // columns are calculated based on utf-16 + pub column: usize, +} + +impl Location { + pub fn bump_line(&mut self) { + self.line += 1; + self.column = 0; + } +} + +#[test] +fn basic() { + let source = "string\nwizard"; + let locator = Locator::new(source); + + assert_eq!(locator.line_offsets[0], 0); + assert_eq!(locator.line_offsets[1], 7); + + assert_eq!(locator.locate(0), Location { line: 0, column: 0 }); + assert_eq!(locator.locate(12), Location { line: 1, column: 5 }); + assert_eq!(locator.locate(7), Location { line: 1, column: 0 }); + assert_eq!(locator.locate(1), Location { line: 0, column: 1 }); + assert_eq!(locator.locate(8), Location { line: 1, column: 1 }); +} + +#[test] +fn special_chars() { + let source = "ƟšŸ’£\nšŸ’£ĆŸ"; + let locator = Locator::new(source); + assert_eq!(locator.line_offsets[0], 0); + assert_eq!(locator.line_offsets[1], 4); + + assert_eq!(locator.locate(0), Location { line: 0, column: 0 }); + assert_eq!(locator.locate(4), Location { line: 1, column: 0 }); + assert_eq!(locator.locate(6), Location { line: 1, column: 2 }); +} + +#[test] +fn edge_cases() { + let locator = Locator::new(""); + assert_eq!(locator.line_offsets.len(), 1); + assert_eq!(locator.locate(0), Location { line: 0, column: 0 }); +} diff --git a/src/source_map/mod.rs b/src/source_map/mod.rs new file mode 100644 index 0000000..e2d225e --- /dev/null +++ b/src/source_map/mod.rs @@ -0,0 +1,2 @@ +pub mod locator; +pub mod sourcemap_builder; diff --git a/src/source_map/sourcemap_builder.rs b/src/source_map/sourcemap_builder.rs new file mode 100644 index 0000000..6c7baee --- /dev/null +++ b/src/source_map/sourcemap_builder.rs @@ -0,0 +1,104 @@ +use crate::chunk::Chunk; + +use super::locator::Locator; + +pub struct SourcemapBuilder { + hires: bool, + generated_code_line: usize, + /// `generated_code_column` is calculated based on utf-16. + generated_code_column: usize, + source_id: u32, + source_map_builder: oxc_sourcemap::SourceMapBuilder, +} + +impl SourcemapBuilder { + pub fn new(hires: bool) -> Self { + Self { + hires, + generated_code_line: 0, + generated_code_column: 0, + source_id: 0, + source_map_builder: oxc_sourcemap::SourceMapBuilder::default(), + } + } + + pub fn into_source_map(self) -> oxc_sourcemap::SourceMap { + self.source_map_builder.into_sourcemap() + } + + pub fn set_source_and_content(&mut self, id: &str, content: &str) { + self.source_id = self.source_map_builder.set_source_and_content(id, content); + } + + pub fn add_chunk(&mut self, chunk: &Chunk, locator: &Locator, source: &str, name: Option<&str>) { + let name_id = if chunk.keep_in_mappings { + name.map(|name| self.source_map_builder.add_name(name)) + } else { + None + }; + let mut loc = locator.locate(chunk.start()); + if let Some(edited_content) = &chunk.edited_content { + if !edited_content.is_empty() { + self.source_map_builder.add_token( + self.generated_code_line as u32, + self.generated_code_column as u32, + loc.line as u32, + loc.column as u32, + Some(self.source_id), + name_id, + ); + } + self.advance(edited_content); + } else { + let chunk_content = chunk.span.text(source); + let mut new_line = true; + for char in chunk_content.chars() { + // TODO support hires boundary + if new_line || self.hires { + self.source_map_builder.add_token( + self.generated_code_line as u32, + self.generated_code_column as u32, + loc.line as u32, + loc.column as u32, + Some(self.source_id), + name_id, + ); + } + match char { + '\n' => { + loc.bump_line(); + self.bump_line(); + new_line = true; + } + _ => { + let char_utf16_len = char.len_utf16(); + loc.column += char_utf16_len; + self.generated_code_column += char_utf16_len; + new_line = false; + } + } + } + } + } + + pub fn advance(&mut self, content: &str) { + if content.is_empty() { + return; + } + let mut lines = content.split('\n'); + + // SAFETY: In any cases, lines would have at least one element. + // "".split('\n') would create `[""]`. + // "\n".split('\n') would create `["", ""]`. + let last_line = unsafe { lines.next_back().unwrap_unchecked() }; + for _ in lines { + self.bump_line(); + } + self.generated_code_column += last_line.chars().map(|c| c.len_utf16()).sum::(); + } + + fn bump_line(&mut self) { + self.generated_code_line += 1; + self.generated_code_column = 0; + } +} diff --git a/src/sourcemap_builder.rs b/src/sourcemap_builder.rs deleted file mode 100644 index c0c04e1..0000000 --- a/src/sourcemap_builder.rs +++ /dev/null @@ -1,104 +0,0 @@ -use crate::{chunk::Chunk, locator::Locator, TextSize}; - -pub struct SourcemapBuilder { - hires: bool, - generated_code_line: TextSize, - /// `generated_code_column` is calculated based on utf-16. - generated_code_column: TextSize, - source_id: u32, - source_map_builder: oxc_sourcemap::SourceMapBuilder, -} - -impl SourcemapBuilder { - pub fn new(hires: bool) -> Self { - Self { - hires, - generated_code_line: 0, - generated_code_column: 0, - source_id: 0, - source_map_builder: oxc_sourcemap::SourceMapBuilder::default(), - } - } - - pub fn into_source_map(self) -> oxc_sourcemap::SourceMap { - self.source_map_builder.into_sourcemap() - } - - pub fn set_source_and_content(&mut self, id: &str, content: &str) { - self.source_id = self.source_map_builder.set_source_and_content(id, content); - } - - pub fn add_chunk( - &mut self, - chunk: &Chunk, - locator: &Locator, - source: &str, - name: Option<&str>, - ) { - let name_id = name.map(|name| self.source_map_builder.add_name(name)); - let mut loc = locator.locate(chunk.start()); - if let Some(edited_content) = &chunk.edited_content { - if !edited_content.is_empty() { - self.source_map_builder.add_token( - self.generated_code_line, - self.generated_code_column, - loc.line, - loc.column, - Some(self.source_id), - name_id, - ); - } - self.advance(edited_content); - } else { - let chunk_content = chunk.span.text(source); - let mut new_line = true; - for char in chunk_content.chars() { - // TODO support hires boundary - if new_line || self.hires { - self.source_map_builder.add_token( - self.generated_code_line, - self.generated_code_column, - loc.line, - loc.column, - Some(self.source_id), - name_id, - ); - } - match char { - '\n' => { - loc.bump_line(); - self.bump_line(); - new_line = true; - } - _ => { - let char_utf16_len = char.len_utf16() as u32; - loc.column += char_utf16_len; - self.generated_code_column += char_utf16_len; - new_line = false; - } - } - } - } - } - - pub fn advance(&mut self, content: &str) { - if content.is_empty() { - return; - } - let mut lines = content.split('\n'); - - // SAFETY: In any cases, lines would have at least one element. - // "".split('\n') would create `[""]`. - // "\n".split('\n') would create `["", ""]`. - let last_line = unsafe { lines.next_back().unwrap_unchecked() }; - for _ in lines { - self.bump_line(); - } - self.generated_code_column += last_line.chars().map(|c| c.len_utf16() as u32).sum::(); - } - - fn bump_line(&mut self) { - self.generated_code_line += 1; - self.generated_code_column = 0; - } -} diff --git a/src/span.rs b/src/span.rs index c2e001f..2c8b314 100644 --- a/src/span.rs +++ b/src/span.rs @@ -1,20 +1,18 @@ -use crate::TextSize; - #[derive(Debug, Default, Clone, Copy)] -pub struct Span(pub TextSize, pub TextSize); +pub struct Span(pub usize, pub usize); impl Span { - pub fn start(&self) -> TextSize { - self.0 - } + pub fn start(&self) -> usize { + self.0 + } - pub fn end(&self) -> TextSize { - self.1 - } + pub fn end(&self) -> usize { + self.1 + } - pub fn text<'s>(&self, source: &'s str) -> &'s str { - // This crate doesn't support usize which is u16 on 16-bit platforms. - // So, we can safely cast TextSize/u32 to usize. - &source[self.start() as usize..self.end() as usize] - } + pub fn text<'s>(&self, source: &'s str) -> &'s str { + // This crate doesn't support usize which is u16 on 16-bit platforms. + // So, we can safely cast usize/u32 to usize. + &source[self.start() as usize..self.end() as usize] + } } diff --git a/src/type_aliases.rs b/src/type_aliases.rs new file mode 100644 index 0000000..b326724 --- /dev/null +++ b/src/type_aliases.rs @@ -0,0 +1,5 @@ +use index_vec::IndexVec; + +use crate::chunk::{Chunk, ChunkIdx}; + +pub type IndexChunks<'text> = IndexVec>; diff --git a/tests/joiner.rs b/tests/joiner.rs index f052f5f..bf8c00a 100644 --- a/tests/joiner.rs +++ b/tests/joiner.rs @@ -1,23 +1,23 @@ -use string_wizard::{Joiner, MagicString, JoinerOptions}; +use string_wizard::{Joiner, JoinerOptions, MagicString}; mod append { - use super::*; + use super::*; - #[test] - fn should_append_content() { - let mut j = Joiner::default(); - j.append(MagicString::new("*")); - j.append_raw("123").append_raw("456"); - assert_eq!(j.join(), "*123456"); - } + #[test] + fn should_append_content() { + let mut j = Joiner::default(); + j.append(MagicString::new("*")); + j.append_raw("123").append_raw("456"); + assert_eq!(j.join(), "*123456"); + } } #[test] fn separator() { - let mut j = Joiner::with_options(JoinerOptions { separator: Some(",".to_string()) }); - j.append_raw("123"); - assert_eq!(j.join(), "123"); - j.append_raw("123"); - assert_eq!(j.join(), "123,123"); - j.append_raw("123"); - assert_eq!(j.join(), "123,123,123"); -} \ No newline at end of file + let mut j = Joiner::with_options(JoinerOptions { separator: Some(",".to_string()) }); + j.append_raw("123"); + assert_eq!(j.join(), "123"); + j.append_raw("123"); + assert_eq!(j.join(), "123,123"); + j.append_raw("123"); + assert_eq!(j.join(), "123,123,123"); +} diff --git a/tests/magic_string.rs b/tests/magic_string.rs index 26816ef..8fb600f 100644 --- a/tests/magic_string.rs +++ b/tests/magic_string.rs @@ -6,432 +6,402 @@ use string_wizard::MagicStringOptions; use string_wizard::UpdateOptions; trait MagicStringExt<'text> { - fn overwrite( - &mut self, - start: usize, - end: usize, - content: impl Into>, - ) -> &mut Self; - - fn indent_str(&mut self, indent_str: &str) -> &mut Self; + fn overwrite( + &mut self, + start: usize, + end: usize, + content: impl Into>, + ) -> &mut Self; + + fn indent_str(&mut self, indent_str: &str) -> &mut Self; } impl<'text> MagicStringExt<'text> for MagicString<'text> { - fn overwrite( - &mut self, - start: usize, - end: usize, - content: impl Into>, - ) -> &mut Self { - self.update_with( - start, - end, - content, - UpdateOptions { - overwrite: true, - ..Default::default() - }, - ) - } - /// Shortcut for `indent_with(IndentOptions { indent_str: Some(indent_str), ..Default::default() })` - fn indent_str(&mut self, indent_str: &str) -> &mut Self { - self.indent_with(IndentOptions { - indentor: Some(indent_str), - ..Default::default() - }) - } + fn overwrite( + &mut self, + start: usize, + end: usize, + content: impl Into>, + ) -> &mut Self { + self.update_with(start, end, content, UpdateOptions { overwrite: true, ..Default::default() }) + } + /// Shortcut for `indent_with(IndentOptions { indent_str: Some(indent_str), ..Default::default() })` + fn indent_str(&mut self, indent_str: &str) -> &mut Self { + self.indent_with(IndentOptions { indentor: Some(indent_str), ..Default::default() }) + } } mod options { - use super::*; - #[test] - fn stores_source_file_information() { - let s = MagicString::with_options( - "abc", - MagicStringOptions { - filename: Some("foo.js".to_string()), - }, - ); - assert_eq!(s.filename, Some("foo.js".to_string())) - } + use super::*; + #[test] + fn stores_source_file_information() { + let s = + MagicString::with_options("abc", MagicStringOptions { filename: Some("foo.js".to_string()) }); + assert_eq!(s.filename, Some("foo.js".to_string())) + } } mod append { - use super::*; - - #[test] - fn should_append_content() { - // should append content - let mut s = MagicString::new("abcdefghijkl"); - s.append("xyz"); - assert_eq!(s.to_string(), "abcdefghijklxyz"); - s.append("xyz"); - assert_eq!(s.to_string(), "abcdefghijklxyzxyz"); - } + use super::*; + + #[test] + fn should_append_content() { + // should append content + let mut s = MagicString::new("abcdefghijkl"); + s.append("xyz"); + assert_eq!(s.to_string(), "abcdefghijklxyz"); + s.append("xyz"); + assert_eq!(s.to_string(), "abcdefghijklxyzxyz"); + } } mod prepend_append_left_right { - use super::*; - - #[test] - fn preserves_intended_order() { - let mut s = MagicString::new("0123456789"); - s.append_left(5, "A"); - assert_eq!(s.to_string(), "01234A56789"); - s.prepend_right(5, "a"); - s.prepend_right(5, "b"); - s.append_left(5, "B"); - s.append_left(5, "C"); - s.prepend_right(5, "c"); - - assert_eq!(s.to_string(), "01234ABCcba56789"); - - s.prepend_left(5, "<"); - s.prepend_left(5, "{"); - assert_eq!(s.to_string(), "01234{"); - s.append_right(5, "}"); - assert_eq!(s.to_string(), "01234{}56789"); - - s.append_left(5, "("); - s.append_left(5, "["); - assert_eq!(s.to_string(), "01234{}56789"); - - s.prepend_right(5, ")"); - s.prepend_right(5, "]"); - assert_eq!(s.to_string(), "01234{}56789"); - } - - #[test] - fn preserves_intended_order_at_beginning_of_string() { - let mut s = MagicString::new("x"); - s.append_left(0, "1"); - s.prepend_left(0, "2"); - s.append_left(0, "3"); - s.prepend_left(0, "4"); - - assert_eq!(s.to_string(), "4213x"); - } - - #[test] - fn preserves_intended_order_at_end_of_string() { - let mut s = MagicString::new("x"); - s.append_right(1, "1"); - s.prepend_right(1, "2"); - s.append_right(1, "3"); - s.prepend_right(1, "4"); - - assert_eq!(s.to_string(), "x4213"); - } + use super::*; + + #[test] + fn preserves_intended_order() { + let mut s = MagicString::new("0123456789"); + s.append_left(5, "A"); + assert_eq!(s.to_string(), "01234A56789"); + s.prepend_right(5, "a"); + s.prepend_right(5, "b"); + s.append_left(5, "B"); + s.append_left(5, "C"); + s.prepend_right(5, "c"); + + assert_eq!(s.to_string(), "01234ABCcba56789"); + + s.prepend_left(5, "<"); + s.prepend_left(5, "{"); + assert_eq!(s.to_string(), "01234{"); + s.append_right(5, "}"); + assert_eq!(s.to_string(), "01234{}56789"); + + s.append_left(5, "("); + s.append_left(5, "["); + assert_eq!(s.to_string(), "01234{}56789"); + + s.prepend_right(5, ")"); + s.prepend_right(5, "]"); + assert_eq!(s.to_string(), "01234{}56789"); + } + + #[test] + fn preserves_intended_order_at_beginning_of_string() { + let mut s = MagicString::new("x"); + s.append_left(0, "1"); + s.prepend_left(0, "2"); + s.append_left(0, "3"); + s.prepend_left(0, "4"); + + assert_eq!(s.to_string(), "4213x"); + } + + #[test] + fn preserves_intended_order_at_end_of_string() { + let mut s = MagicString::new("x"); + s.append_right(1, "1"); + s.prepend_right(1, "2"); + s.append_right(1, "3"); + s.prepend_right(1, "4"); + + assert_eq!(s.to_string(), "x4213"); + } } mod clone { - use super::*; + use super::*; - #[test] - fn should_clone_a_magic_string() { - let mut s = MagicString::new("abcdefghijkl"); - s.overwrite(3, 9, "XYZ"); - let c = s.clone(); + #[test] + fn should_clone_a_magic_string() { + let mut s = MagicString::new("abcdefghijkl"); + s.overwrite(3, 9, "XYZ"); + let c = s.clone(); - assert_eq!(c.to_string(), "abcXYZjkl") - } + assert_eq!(c.to_string(), "abcXYZjkl") + } } mod overwrite { - use super::*; - - #[test] - fn should_replace_characters() { - let mut s = MagicString::new("abcdefghijkl"); - s.overwrite(5, 8, "FGH"); - assert_eq!(s.to_string(), "abcdeFGHijkl"); - } - - // #[test] - // fn should_throw_an_error_if_overlapping_replacements_are_attempted() { - // let mut s = MagicString::new("abcdefghijkl"); - // s.overwrite(7, 11, "xx"); - // assert!(std::panic::catch_unwind(|| { - // s.clone().overwrite(8, 12, "yy"); - // }) - // .is_err()) - // } + use super::*; + + #[test] + fn should_replace_characters() { + let mut s = MagicString::new("abcdefghijkl"); + s.overwrite(5, 8, "FGH"); + assert_eq!(s.to_string(), "abcdeFGHijkl"); + } + + // #[test] + // fn should_throw_an_error_if_overlapping_replacements_are_attempted() { + // let mut s = MagicString::new("abcdefghijkl"); + // s.overwrite(7, 11, "xx"); + // assert!(std::panic::catch_unwind(|| { + // s.clone().overwrite(8, 12, "yy"); + // }) + // .is_err()) + // } } mod relocate { - use super::*; - - #[test] - fn moves_content_from_the_start() { - let mut s = MagicString::new("abcdefghijkl"); - s.relocate(0, 3, 6); - assert_eq!(s.to_string(), "defabcghijkl"); - } - - #[test] - fn moves_content_to_the_start() { - let mut s = MagicString::new("abcdefghijkl"); - s.relocate(3, 6, 0); - assert_eq!(s.to_string(), "defabcghijkl"); - } - - #[test] - fn moves_content_from_the_end() { - let mut s = MagicString::new("abcdefghijkl"); - s.relocate(9, 12, 6); - assert_eq!(s.to_string(), "abcdefjklghi"); - } - - #[test] - fn moves_content_to_the_end() { - let mut s = MagicString::new("abcdefghijkl"); - s.relocate(6, 9, 12); - assert_eq!(s.to_string(), "abcdefjklghi"); - } - - #[test] - fn ignores_redundant_move() { - let mut s = MagicString::new("abcdefghijkl"); - s.prepend_right(9, "X") - .relocate(9, 12, 6) - .append_left(12, "Y") - // this is redundant ā€“ [6,9] is already after [9,12] - .relocate(6, 9, 12); - - assert_eq!(s.to_string(), "abcdefXjklYghi"); - } - - #[test] - fn moves_content_to_the_middle() { - let mut s = MagicString::new("abcdefghijkl"); - s.relocate(3, 6, 9); - assert_eq!(s.to_string(), "abcghidefjkl"); - } - - #[test] - fn handles_multiple_moves_of_the_same_snippet() { - let mut s = MagicString::new("abcdefghijkl"); - - s.relocate(0, 3, 6); - assert_eq!(s.to_string(), "defabcghijkl"); - - s.relocate(0, 3, 9); - assert_eq!(s.to_string(), "defghiabcjkl"); - } - - #[test] - fn handles_moves_of_adjacent_snippets() { - let mut s = MagicString::new("abcdefghijkl"); - - s.relocate(0, 2, 6); - assert_eq!(s.to_string(), "cdefabghijkl"); - s.relocate(2, 4, 6); - assert_eq!(s.to_string(), "efabcdghijkl"); - } - - #[test] - fn handles_moves_to_same_index() { - let mut s = MagicString::new("abcdefghijkl"); - s.relocate(0, 2, 6).relocate(3, 5, 6); - assert_eq!(s.to_string(), "cfabdeghijkl"); - } - - #[test] - #[should_panic] - fn refuses_to_move_a_selection_to_inside_itself() { - let mut s = MagicString::new("abcdefghijkl"); - s.relocate(3, 6, 3); - } - #[test] - #[should_panic] - fn refuses_to_move_a_selection_to_inside_itself2() { - let mut s = MagicString::new("abcdefghijkl"); - s.relocate(3, 6, 4); - } - #[test] - #[should_panic] - fn refuses_to_move_a_selection_to_inside_itself3() { - let mut s = MagicString::new("abcdefghijkl"); - s.relocate(3, 6, 6); - } - - #[test] - fn allows_edits_of_moved_content() { - let mut s1 = MagicString::new("abcdefghijkl"); - s1.relocate(3, 6, 9); - s1.overwrite(3, 6, "DEF"); - assert_eq!(s1.to_string(), "abcghiDEFjkl"); - let mut s2 = MagicString::new("abcdefghijkl"); - s2.relocate(3, 6, 9); - s2.overwrite(4, 5, "E"); - assert_eq!(s2.to_string(), "abcghidEfjkl"); - } - - #[test] - fn moves_content_inserted_at_end_of_range() { - let mut s = MagicString::new("abcdefghijkl"); - s.append_left(6, "X").relocate(3, 6, 9); - assert_eq!(s.to_string(), "abcghidefXjkl"); - } + use super::*; + + #[test] + fn moves_content_from_the_start() { + let mut s = MagicString::new("abcdefghijkl"); + s.relocate(0, 3, 6); + assert_eq!(s.to_string(), "defabcghijkl"); + } + + #[test] + fn moves_content_to_the_start() { + let mut s = MagicString::new("abcdefghijkl"); + s.relocate(3, 6, 0); + assert_eq!(s.to_string(), "defabcghijkl"); + } + + #[test] + fn moves_content_from_the_end() { + let mut s = MagicString::new("abcdefghijkl"); + s.relocate(9, 12, 6); + assert_eq!(s.to_string(), "abcdefjklghi"); + } + + #[test] + fn moves_content_to_the_end() { + let mut s = MagicString::new("abcdefghijkl"); + s.relocate(6, 9, 12); + assert_eq!(s.to_string(), "abcdefjklghi"); + } + + #[test] + fn ignores_redundant_move() { + let mut s = MagicString::new("abcdefghijkl"); + s.prepend_right(9, "X") + .relocate(9, 12, 6) + .append_left(12, "Y") + // this is redundant ā€“ [6,9] is already after [9,12] + .relocate(6, 9, 12); + + assert_eq!(s.to_string(), "abcdefXjklYghi"); + } + + #[test] + fn moves_content_to_the_middle() { + let mut s = MagicString::new("abcdefghijkl"); + s.relocate(3, 6, 9); + assert_eq!(s.to_string(), "abcghidefjkl"); + } + + #[test] + fn handles_multiple_moves_of_the_same_snippet() { + let mut s = MagicString::new("abcdefghijkl"); + + s.relocate(0, 3, 6); + assert_eq!(s.to_string(), "defabcghijkl"); + + s.relocate(0, 3, 9); + assert_eq!(s.to_string(), "defghiabcjkl"); + } + + #[test] + fn handles_moves_of_adjacent_snippets() { + let mut s = MagicString::new("abcdefghijkl"); + + s.relocate(0, 2, 6); + assert_eq!(s.to_string(), "cdefabghijkl"); + s.relocate(2, 4, 6); + assert_eq!(s.to_string(), "efabcdghijkl"); + } + + #[test] + fn handles_moves_to_same_index() { + let mut s = MagicString::new("abcdefghijkl"); + s.relocate(0, 2, 6).relocate(3, 5, 6); + assert_eq!(s.to_string(), "cfabdeghijkl"); + } + + #[test] + #[should_panic] + fn refuses_to_move_a_selection_to_inside_itself() { + let mut s = MagicString::new("abcdefghijkl"); + s.relocate(3, 6, 3); + } + #[test] + #[should_panic] + fn refuses_to_move_a_selection_to_inside_itself2() { + let mut s = MagicString::new("abcdefghijkl"); + s.relocate(3, 6, 4); + } + #[test] + #[should_panic] + fn refuses_to_move_a_selection_to_inside_itself3() { + let mut s = MagicString::new("abcdefghijkl"); + s.relocate(3, 6, 6); + } + + #[test] + fn allows_edits_of_moved_content() { + let mut s1 = MagicString::new("abcdefghijkl"); + s1.relocate(3, 6, 9); + s1.overwrite(3, 6, "DEF"); + assert_eq!(s1.to_string(), "abcghiDEFjkl"); + let mut s2 = MagicString::new("abcdefghijkl"); + s2.relocate(3, 6, 9); + s2.overwrite(4, 5, "E"); + assert_eq!(s2.to_string(), "abcghidEfjkl"); + } + + #[test] + fn moves_content_inserted_at_end_of_range() { + let mut s = MagicString::new("abcdefghijkl"); + s.append_left(6, "X").relocate(3, 6, 9); + assert_eq!(s.to_string(), "abcghidefXjkl"); + } } mod indent { - use string_wizard::IndentOptions; - - use super::*; - - #[test] - fn should_indent_content_with_a_single_tab_character_by_default() { - let mut s = MagicString::new("abc\ndef\nghi\njkl"); - s.indent(); - assert_eq!(s.to_string(), "\tabc\n\tdef\n\tghi\n\tjkl") - } - - #[test] - fn should_indent_content_with_a_single_tab_character_by_default2() { - let mut s = MagicString::new(""); - s.prepend("abc\ndef\nghi\njkl"); - s.indent(); - assert_eq!(s.to_string(), "\tabc\n\tdef\n\tghi\n\tjkl"); - let mut s = MagicString::new(""); - s.prepend("abc\ndef"); - s.append("\nghi\njkl"); - s.indent(); - assert_eq!(s.to_string(), "\tabc\n\tdef\n\tghi\n\tjkl") - } - - #[test] - fn should_indent_content_using_existing_indentation_as_a_guide() { - 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"); - } - - #[test] - fn should_disregard_single_space_indentation_when_auto_indenting() { - 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 */"); - } - - #[test] - fn should_indent_content_using_the_supplied_indent_string() { - let mut s = MagicString::new("abc\ndef\nghi\njkl"); - s.indent_str(" "); - assert_eq!(s.to_string(), " abc\n def\n ghi\n jkl"); - s.indent_str(">>"); - assert_eq!(s.to_string(), ">> abc\n>> def\n>> ghi\n>> jkl"); - } - - #[test] - fn should_indent_content_using_the_empty_string_if_specified() { - // should indent content using the empty string if specified (i.e. noop) - let mut s = MagicString::new("abc\ndef\nghi\njkl"); - s.indent_str(""); - assert_eq!(s.to_string(), "abc\ndef\nghi\njkl"); - } - #[test] - fn should_prevent_excluded_characters_from_being_indented() { - let mut s = MagicString::new("abc\ndef\nghi\njkl"); - s.indent_with(IndentOptions { - indentor: Some(" "), - exclude: &[(7, 15)], - }); - assert_eq!(s.to_string(), " abc\n def\nghi\njkl"); - s.indent_with(IndentOptions { - indentor: Some(">>"), - exclude: &[(7, 15)], - }); - assert_eq!(s.to_string(), ">> abc\n>> def\nghi\njkl"); - } - - #[test] - fn should_not_add_characters_to_empty_lines() { - // should indent content using the empty string if specified (i.e. noop) - let mut s = MagicString::new("\n\nabc\ndef\n\nghi\njkl"); - s.indent(); - assert_eq!(s.to_string(), "\n\n\tabc\n\tdef\n\n\tghi\n\tjkl"); - s.indent(); - assert_eq!(s.to_string(), "\n\n\t\tabc\n\t\tdef\n\n\t\tghi\n\t\tjkl"); - } - - #[test] - fn should_not_add_characters_to_empty_lines_even_on_windows() { - // should indent content using the empty string if specified (i.e. noop) - let mut s = MagicString::new("\r\n\r\nabc\r\ndef\r\n\r\nghi\r\njkl"); - s.indent(); - assert_eq!( - s.to_string(), - "\r\n\r\n\tabc\r\n\tdef\r\n\r\n\tghi\r\n\tjkl" - ); - s.indent(); - 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" - ); - } - - #[test] - fn should_indent_content_with_removals() { - let mut s = MagicString::new("/* remove this line */\nvar foo = 1;"); - // remove `/* remove this line */\n` - s.remove(0, 23); - s.indent(); - assert_eq!(s.to_string(), "\tvar foo = 1;"); - } - - #[test] - fn should_not_indent_patches_in_the_middle_of_a_line() { - let mut s = MagicString::new("class Foo extends Bar {}"); - s.overwrite(18, 21, "Baz"); - assert_eq!(s.to_string(), "class Foo extends Baz {}"); - s.indent(); - assert_eq!(s.to_string(), "\tclass Foo extends Baz {}"); - } - - #[test] - fn should_ignore_the_end_of_each_exclude_range() { - let mut s = MagicString::new("012\n456\n89a\nbcd"); - s.indent_with(IndentOptions { - indentor: Some(">"), - exclude: &[(0, 3)], - }); - assert_eq!(s.to_string(), "012\n>456\n>89a\n>bcd"); - } + use string_wizard::IndentOptions; + + use super::*; + + #[test] + fn should_indent_content_with_a_single_tab_character_by_default() { + let mut s = MagicString::new("abc\ndef\nghi\njkl"); + s.indent(); + assert_eq!(s.to_string(), "\tabc\n\tdef\n\tghi\n\tjkl") + } + + #[test] + fn should_indent_content_with_a_single_tab_character_by_default2() { + let mut s = MagicString::new(""); + s.prepend("abc\ndef\nghi\njkl"); + s.indent(); + assert_eq!(s.to_string(), "\tabc\n\tdef\n\tghi\n\tjkl"); + let mut s = MagicString::new(""); + s.prepend("abc\ndef"); + s.append("\nghi\njkl"); + s.indent(); + assert_eq!(s.to_string(), "\tabc\n\tdef\n\tghi\n\tjkl") + } + + #[test] + fn should_indent_content_using_existing_indentation_as_a_guide() { + 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"); + } + + #[test] + fn should_disregard_single_space_indentation_when_auto_indenting() { + 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 */"); + } + + #[test] + fn should_indent_content_using_the_supplied_indent_string() { + let mut s = MagicString::new("abc\ndef\nghi\njkl"); + s.indent_str(" "); + assert_eq!(s.to_string(), " abc\n def\n ghi\n jkl"); + s.indent_str(">>"); + assert_eq!(s.to_string(), ">> abc\n>> def\n>> ghi\n>> jkl"); + } + + #[test] + fn should_indent_content_using_the_empty_string_if_specified() { + // should indent content using the empty string if specified (i.e. noop) + let mut s = MagicString::new("abc\ndef\nghi\njkl"); + s.indent_str(""); + assert_eq!(s.to_string(), "abc\ndef\nghi\njkl"); + } + #[test] + fn should_prevent_excluded_characters_from_being_indented() { + let mut s = MagicString::new("abc\ndef\nghi\njkl"); + s.indent_with(IndentOptions { indentor: Some(" "), exclude: &[(7, 15)] }); + assert_eq!(s.to_string(), " abc\n def\nghi\njkl"); + s.indent_with(IndentOptions { indentor: Some(">>"), exclude: &[(7, 15)] }); + assert_eq!(s.to_string(), ">> abc\n>> def\nghi\njkl"); + } + + #[test] + fn should_not_add_characters_to_empty_lines() { + // should indent content using the empty string if specified (i.e. noop) + let mut s = MagicString::new("\n\nabc\ndef\n\nghi\njkl"); + s.indent(); + assert_eq!(s.to_string(), "\n\n\tabc\n\tdef\n\n\tghi\n\tjkl"); + s.indent(); + assert_eq!(s.to_string(), "\n\n\t\tabc\n\t\tdef\n\n\t\tghi\n\t\tjkl"); + } + + #[test] + fn should_not_add_characters_to_empty_lines_even_on_windows() { + // should indent content using the empty string if specified (i.e. noop) + let mut s = MagicString::new("\r\n\r\nabc\r\ndef\r\n\r\nghi\r\njkl"); + s.indent(); + assert_eq!(s.to_string(), "\r\n\r\n\tabc\r\n\tdef\r\n\r\n\tghi\r\n\tjkl"); + s.indent(); + 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"); + } + + #[test] + fn should_indent_content_with_removals() { + let mut s = MagicString::new("/* remove this line */\nvar foo = 1;"); + // remove `/* remove this line */\n` + s.remove(0, 23); + s.indent(); + assert_eq!(s.to_string(), "\tvar foo = 1;"); + } + + #[test] + fn should_not_indent_patches_in_the_middle_of_a_line() { + let mut s = MagicString::new("class Foo extends Bar {}"); + s.overwrite(18, 21, "Baz"); + assert_eq!(s.to_string(), "class Foo extends Baz {}"); + s.indent(); + assert_eq!(s.to_string(), "\tclass Foo extends Baz {}"); + } + + #[test] + fn should_ignore_the_end_of_each_exclude_range() { + let mut s = MagicString::new("012\n456\n89a\nbcd"); + s.indent_with(IndentOptions { indentor: Some(">"), exclude: &[(0, 3)] }); + assert_eq!(s.to_string(), "012\n>456\n>89a\n>bcd"); + } } mod misc { - use super::*; - - #[test] - fn should_append_content() { - // should append content - let mut s = MagicString::new("abcdefghijkl"); - s.prepend("xyz"); - assert_eq!(s.to_string(), "xyzabcdefghijkl"); - s.prepend("xyz"); - assert_eq!(s.to_string(), "xyzxyzabcdefghijkl"); - } - - #[test] - fn remove() { - // should append content - let mut s = MagicString::new("0123456"); - assert_eq!(s.remove(0, 3).to_string(), "3456"); - assert_eq!(s.remove(3, 7).to_string(), ""); - } - - #[test] - fn allow_empty_input() { - let mut s = MagicString::new(""); - s.append("xyz"); - assert_eq!(s.to_string(), "xyz"); - s.prepend("xyz"); - assert_eq!(s.to_string(), "xyzxyz"); - } + use super::*; + + #[test] + fn should_append_content() { + // should append content + let mut s = MagicString::new("abcdefghijkl"); + s.prepend("xyz"); + assert_eq!(s.to_string(), "xyzabcdefghijkl"); + s.prepend("xyz"); + assert_eq!(s.to_string(), "xyzxyzabcdefghijkl"); + } + + #[test] + fn remove() { + // should append content + let mut s = MagicString::new("0123456"); + assert_eq!(s.remove(0, 3).to_string(), "3456"); + assert_eq!(s.remove(3, 7).to_string(), ""); + } + + #[test] + fn allow_empty_input() { + let mut s = MagicString::new(""); + s.append("xyz"); + assert_eq!(s.to_string(), "xyz"); + s.prepend("xyz"); + assert_eq!(s.to_string(), "xyzxyz"); + } } diff --git a/tests/magic_string_source_map.rs b/tests/magic_string_source_map.rs index 6d199c5..8509760 100644 --- a/tests/magic_string_source_map.rs +++ b/tests/magic_string_source_map.rs @@ -2,48 +2,31 @@ use string_wizard::{MagicString, SourceMapOptions, UpdateOptions}; #[test] fn basic() { - let input = "
\n hello, world\n
"; - let mut s = MagicString::new(input); - let update_options = UpdateOptions { - keep_original: true, - ..Default::default() - }; - s.update_with(1, 2, "v", update_options.clone()) - .update_with(3, 4, "d", update_options.clone()) - .update_with( - input.len() - 4, - input.len() - 1, - "h1", - update_options.clone(), - ); + let input = "
\n hello, world\n
"; + let mut s = MagicString::new(input); + let update_options = UpdateOptions { keep_original: true, ..Default::default() }; + s.update_with(1, 2, "v", update_options.clone()) + .update_with(3, 4, "d", update_options.clone()) + .update_with(input.len() - 4, input.len() - 1, "h1", update_options.clone()); - let sm = s.source_map(SourceMapOptions { - include_content: true, - ..Default::default() - }); + let sm = s.source_map(SourceMapOptions { include_content: true, ..Default::default() }); - assert_eq!( - sm.to_json_string().unwrap(), + assert_eq!( + sm.to_json_string(), "{\"version\":3,\"names\":[\"d\",\"v\",\"div\"],\"sources\":[\"\"],\"sourcesContent\":[\"
\\n hello, world\\n
\"],\"mappings\":\"AAAA,CAACA,CAAC,CAACC,CAAC;AACJ;AACA,EAAEC,EAAG\"}" ); - s.prepend("import React from 'react';\n"); - let sm = s.source_map(SourceMapOptions { - include_content: true, - ..Default::default() - }); - assert_eq!( - sm.to_json_string().unwrap(), + s.prepend("import React from 'react';\n"); + let sm = s.source_map(SourceMapOptions { include_content: true, ..Default::default() }); + assert_eq!( + sm.to_json_string(), "{\"version\":3,\"names\":[\"d\",\"v\",\"div\"],\"sources\":[\"\"],\"sourcesContent\":[\"
\\n hello, world\\n
\"],\"mappings\":\";AAAA,CAACA,CAAC,CAACC,CAAC;AACJ;AACA,EAAEC,EAAG\"}" ); - let sm = s.source_map(SourceMapOptions { - include_content: true, - hires: true, - ..Default::default() - }); - assert_eq!( - sm.to_json_string().unwrap(), + let sm = + s.source_map(SourceMapOptions { include_content: true, hires: true, ..Default::default() }); + assert_eq!( + sm.to_json_string(), "{\"version\":3,\"names\":[\"d\",\"v\",\"div\"],\"sources\":[\"\"],\"sourcesContent\":[\"
\\n hello, world\\n
\"],\"mappings\":\";AAAA,CAACA,CAAC,CAACC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACd,CAAC,CAACC,EAAG\"}" ); } diff --git a/tests/usage.rs b/tests/usage.rs deleted file mode 100644 index 40b8f67..0000000 --- a/tests/usage.rs +++ /dev/null @@ -1,14 +0,0 @@ -use string_wizard::MagicString; - -#[test] -pub fn should_alow_passing_u32_or_usize() { - let mut s = MagicString::new("x"); - let start_u32 = 0u32; - let end_u32 = 1u32; - s.update(start_u32, end_u32, "y"); - assert_eq!(s.to_string(), "y"); - let start_u32 = 0usize; - let end_u32 = 1usize; - s.update(start_u32, end_u32, "z"); - assert_eq!(s.to_string(), "z"); -} \ No newline at end of file