From bdc2a9469d48bf9dc634b1021db5388f34dd5953 Mon Sep 17 00:00:00 2001 From: faga Date: Thu, 16 Mar 2023 17:48:02 +0800 Subject: [PATCH 1/3] feat: slice api support --- core/src/magic_string.rs | 44 +++++++++++++++++ core/tests/slice.rs | 104 +++++++++++++++++++++++++++++++++++++++ node/src/lib.rs | 6 +++ 3 files changed, 154 insertions(+) create mode 100644 core/tests/slice.rs diff --git a/core/src/magic_string.rs b/core/src/magic_string.rs index 325afde..bdbadfb 100644 --- a/core/src/magic_string.rs +++ b/core/src/magic_string.rs @@ -525,6 +525,50 @@ impl MagicString { Ok(self) } + /// ## Slice + /// Get a slice of the modified string. + /// + /// Example: + /// ``` + /// use magic_string::MagicString; + /// + /// let mut s = MagicString::new("abcdefghijkl"); + /// assert_eq!(s.slice(1,5)?, "bcde"); + /// assert_eq!(s.slice(1, -1)?, "bcdefghijk"); + /// + /// ``` + /// + pub fn slice(&mut self, start: i64, end: i64) -> Result { + let start = normalize_index(self.original_str.as_str(), start)?; + let end = normalize_index(self.original_str.as_str(), end)?; + + let start = start as u32; + let end = end as u32; + + if start > end { + return Err(Error::new_with_reason( + MagicStringErrorType::MagicStringOutOfRangeError, + "Start must be greater than end.", + )); + } + + self._split_at_index(start)?; + self._split_at_index(end)?; + + let first = self.chunk_by_start.get(&start); + let last = self.chunk_by_end.get(&end); + + let mut str = String::new(); + + if first.is_some() && last.is_some() { + Chunk::try_each_next(Rc::clone(first.unwrap()), |chunk| { + str.push_str(chunk.borrow().to_string().as_str()); + Ok(chunk == Rc::clone(last.unwrap())) + })?; + } + + Ok(str) + } /// ## Is empty /// /// Returns `true` if the resulting source is empty (disregarding white space). diff --git a/core/tests/slice.rs b/core/tests/slice.rs new file mode 100644 index 0000000..5fbd644 --- /dev/null +++ b/core/tests/slice.rs @@ -0,0 +1,104 @@ +#[cfg(test)] + +mod slice { + use magic_string::{MagicString, OverwriteOptions, Result}; + + #[test] + fn should_return_the_generated_content_between_the_specified_original_characters() -> Result { + let mut s = MagicString::new("abcdefghijkl"); + + assert_eq!(s.slice(3, 9)?, "defghi"); + s.overwrite(4, 8, "XX", OverwriteOptions::default())?; + assert_eq!(s.slice(3, 9)?, "dXXi"); + s.overwrite(2, 10, "ZZ", OverwriteOptions::default())?; + assert_eq!(s.slice(1, 11)?, "bZZk"); + assert_eq!(s.slice(2, 10)?, "ZZ"); + + Ok(()) + } + + // #[test] + // fn defaults_end_to_the_original_string_length() -> Result { + // let mut s = MagicString::new("abcdefghijkl"); + // assert_eq!(s.slice(3)?, "defghijkl"); + // } + + #[test] + fn allow_negative_numbers_as_params() -> Result { + let mut s = MagicString::new("abcdefghijkl"); + + assert_eq!(s.slice(0, -3)?, "abcdefghi"); + // assert_eq!(s.slice(-3)?, "jkl"); + Ok(()) + } + #[test] + fn includes_inserted_characters_respecting_insertion_direction() -> Result { + let mut s = MagicString::new("abefij"); + + s.prepend_right(2, "cd")?; + s.append_left(4, "gh")?; + + // assert_eq!(s.slice(), "abcdefghij"); + assert_eq!(s.slice(1, 5)?, "bcdefghi"); + assert_eq!(s.slice(2, 4)?, "cdefgh"); + assert_eq!(s.slice(3, 4)?, "fgh"); + assert_eq!(s.slice(0, 2)?, "ab"); + assert_eq!(s.slice(0, 3)?, "abcde"); + assert_eq!(s.slice(4, 6)?, "ij"); + assert_eq!(s.slice(3, 6)?, "fghij"); + Ok(()) + } + + // wating for move to be implemented + // #[test] + // fn supports_characters_moved_outward() -> Result { + // let mut s = MagicString::new("abcdEFghIJklmn"); + + // s._move(4, 6, 2)?; + // s._move(8, 10, 12)?; + // assert_eq!(s.to_string(), "abEFcdghklIJmn"); + + // assert_eq!(s.slice(1, -1)?, "bEFcdghklIJm"); + // assert_eq!(s.slice(2, -2)?, "cdghkl"); + // assert_eq!(s.slice(3, -3)?, "dghk"); + // assert_eq!(s.slice(4, -4)?, "EFcdghklIJ"); + // assert_eq!(s.slice(5, -5)?, "FcdghklI"); + // assert_eq!(s.slice(6, -6)?, "gh"); + // Ok(()) + // } + + // #[test] + // fn supports_characters_moved_inward() -> Result { + // let mut s = MagicString::new("abCDefghijKLmn"); + // s._move(2, 4, 6)?; + // s._move(10, 12, 8)?; + // assert_eq!(s.to_string(), "abefCDghKLijmn"); + + // assert_eq!(s.slice(1, -1)?, "befCDghKLijm"); + // assert_eq!(s.slice(2, -2)?, "CDghKL"); + // assert_eq!(s.slice(3, -3)?, "DghK"); + // assert_eq!(s.slice(4, -4)?, "efCDghKLij"); + // assert_eq!(s.slice(5, -5)?, "fCDghKLi"); + // assert_eq!(s.slice(6, -6)?, "gh"); + // Ok(()) + // } + + // #[test] + // fn supports_characters_moved_inward() -> Result { + // let mut s = MagicString::new("abCDefghIJkl"); + // // s._move(2, 4, 8)?; + // // s._move(8, 10, 4)?; + // assert_eq!(s.to_string(), "abIJefghCDkl"); + + // assert_eq!(s.slice(1, -1)?, "bIJefghCDk"); + // assert_eq!(s.slice(2, -2)?, ""); + // assert_eq!(s.slice(3, -3)?, ""); + // assert_eq!(s.slice(-3, 3)?, "JefghC"); + // assert_eq!(s.slice(4, -4)?, "efgh"); + // assert_eq!(s.slice(0, 3)?, "abIJefghC"); + // // assert_eq!(s.slice(3)?, "Dkl"); + // assert_eq!(s.slice(0, -3)?, "abI"); + // // assert_eq!(s.slice(-3)?, "JefghCDkl"); + // Ok(()) + // } +} diff --git a/node/src/lib.rs b/node/src/lib.rs index 224f16d..5d52fbc 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -104,6 +104,12 @@ impl MagicString { Ok(self) } + #[napi] + pub fn slice(&mut self, start: i64, end: i64) -> Result<&Self> { + self.0.slice(start, end)?; + Ok(self) + } + #[napi] pub fn is_empty(&self) -> Result { Ok(self.0.is_empty()) From 8debfd0152a8d03403b68c3268d650d75a597210 Mon Sep 17 00:00:00 2001 From: faga Date: Thu, 16 Mar 2023 19:37:49 +0800 Subject: [PATCH 2/3] feat(slice): throw err if chunk has edited --- core/src/chunk.rs | 4 + core/src/magic_string.rs | 7 ++ node/index.d.ts | 1 + node/src/lib.rs | 5 +- node/tests/MagicString.spec.ts | 178 ++++++++++++++++----------------- 5 files changed, 103 insertions(+), 92 deletions(-) diff --git a/core/src/chunk.rs b/core/src/chunk.rs index 6dc5a6c..cd2e46f 100644 --- a/core/src/chunk.rs +++ b/core/src/chunk.rs @@ -16,6 +16,8 @@ pub struct Chunk { pub next: Option>>, pub prev: Option>>, + + pub edited: bool, } impl Chunk { @@ -31,6 +33,8 @@ impl Chunk { next: None, prev: None, + + edited: false, } } diff --git a/core/src/magic_string.rs b/core/src/magic_string.rs index bdbadfb..b3f2ee9 100644 --- a/core/src/magic_string.rs +++ b/core/src/magic_string.rs @@ -301,6 +301,7 @@ impl MagicString { } chunk.borrow_mut().content = String::default(); + chunk.borrow_mut().edited = true; if !content_only { chunk.borrow_mut().intro = String::default(); chunk.borrow_mut().outro = String::default(); @@ -561,6 +562,12 @@ impl MagicString { let mut str = String::new(); if first.is_some() && last.is_some() { + if first.clone().unwrap().borrow().edited { + return Err(Error::new_with_reason( + MagicStringErrorType::MagicStringOutOfRangeError, + "Cannot slice a string that has been edited.", + )); + } Chunk::try_each_next(Rc::clone(first.unwrap()), |chunk| { str.push_str(chunk.borrow().to_string().as_str()); Ok(chunk == Rc::clone(last.unwrap())) diff --git a/node/index.d.ts b/node/index.d.ts index 0c37f8c..6e23ea1 100644 --- a/node/index.d.ts +++ b/node/index.d.ts @@ -49,6 +49,7 @@ export class MagicString { trimEnd(pattern?: string | undefined | null): this trimLines(): this remove(start: number, end: number): this + slice(start: number, end: number): string isEmpty(): boolean generateMap(options?: Partial): { toString: () => string diff --git a/node/src/lib.rs b/node/src/lib.rs index 5d52fbc..369f78f 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -105,9 +105,8 @@ impl MagicString { } #[napi] - pub fn slice(&mut self, start: i64, end: i64) -> Result<&Self> { - self.0.slice(start, end)?; - Ok(self) + pub fn slice(&mut self, start: i64, end: i64) -> Result { + Ok(self.0.slice(start, end)?) } #[napi] diff --git a/node/tests/MagicString.spec.ts b/node/tests/MagicString.spec.ts index 989d99c..b44d6e3 100644 --- a/node/tests/MagicString.spec.ts +++ b/node/tests/MagicString.spec.ts @@ -1094,116 +1094,116 @@ describe('remove', () => { // }) }) -// describe('slice', () => { -// it('should return the generated content between the specified original characters', () => { -// const s = new MagicString('abcdefghijkl') +describe('slice', () => { + it('should return the generated content between the specified original characters', () => { + const s = new MagicString('abcdefghijkl') -// assert.equal(s.slice(3, 9), 'defghi') -// s.overwrite(4, 8, 'XX') -// assert.equal(s.slice(3, 9), 'dXXi') -// s.overwrite(2, 10, 'ZZ') -// assert.equal(s.slice(1, 11), 'bZZk') -// assert.equal(s.slice(2, 10), 'ZZ') + assert.equal(s.slice(3, 9), 'defghi') + s.overwrite(4, 8, 'XX') + assert.equal(s.slice(3, 9), 'dXXi') + s.overwrite(2, 10, 'ZZ') + assert.equal(s.slice(1, 11), 'bZZk') + assert.equal(s.slice(2, 10), 'ZZ') -// assert.throws(() => s.slice(3, 9)) -// }) + assert.throws(() => s.slice(3, 9)) + }) -// it('defaults `end` to the original string length', () => { -// const s = new MagicString('abcdefghijkl') -// assert.equal(s.slice(3), 'defghijkl') -// }) + // it('defaults `end` to the original string length', () => { + // const s = new MagicString('abcdefghijkl') + // assert.equal(s.slice(3), 'defghijkl') + // }) -// it('allows negative numbers as arguments', () => { -// const s = new MagicString('abcdefghijkl') -// assert.equal(s.slice(-3), 'jkl') -// assert.equal(s.slice(0, -3), 'abcdefghi') -// }) + it('allows negative numbers as arguments', () => { + const s = new MagicString('abcdefghijkl') + // assert.equal(s.slice(-3), 'jkl') + assert.equal(s.slice(0, -3), 'abcdefghi') + }) -// it('includes inserted characters, respecting insertion direction', () => { -// const s = new MagicString('abefij') + it('includes inserted characters, respecting insertion direction', () => { + const s = new MagicString('abefij') -// s.prependRight(2, 'cd') -// s.appendLeft(4, 'gh') + s.prependRight(2, 'cd') + s.appendLeft(4, 'gh') -// assert.equal(s.slice(), 'abcdefghij') -// assert.equal(s.slice(1, 5), 'bcdefghi') -// assert.equal(s.slice(2, 4), 'cdefgh') -// assert.equal(s.slice(3, 4), 'fgh') -// assert.equal(s.slice(0, 2), 'ab') -// assert.equal(s.slice(0, 3), 'abcde') -// assert.equal(s.slice(4, 6), 'ij') -// assert.equal(s.slice(3, 6), 'fghij') -// }) + // assert.equal(s.slice(), 'abcdefghij') + assert.equal(s.slice(1, 5), 'bcdefghi') + assert.equal(s.slice(2, 4), 'cdefgh') + assert.equal(s.slice(3, 4), 'fgh') + assert.equal(s.slice(0, 2), 'ab') + assert.equal(s.slice(0, 3), 'abcde') + assert.equal(s.slice(4, 6), 'ij') + assert.equal(s.slice(3, 6), 'fghij') + }) -// it('supports characters moved outward', () => { -// const s = new MagicString('abcdEFghIJklmn') + // it('supports characters moved outward', () => { + // const s = new MagicString('abcdEFghIJklmn') -// s.move(4, 6, 2) -// s.move(8, 10, 12) -// assert.equal(s.toString(), 'abEFcdghklIJmn') + // s.move(4, 6, 2) + // s.move(8, 10, 12) + // assert.equal(s.toString(), 'abEFcdghklIJmn') -// assert.equal(s.slice(1, -1), 'bEFcdghklIJm') -// assert.equal(s.slice(2, -2), 'cdghkl') -// assert.equal(s.slice(3, -3), 'dghk') -// assert.equal(s.slice(4, -4), 'EFcdghklIJ') -// assert.equal(s.slice(5, -5), 'FcdghklI') -// assert.equal(s.slice(6, -6), 'gh') -// }) + // assert.equal(s.slice(1, -1), 'bEFcdghklIJm') + // assert.equal(s.slice(2, -2), 'cdghkl') + // assert.equal(s.slice(3, -3), 'dghk') + // assert.equal(s.slice(4, -4), 'EFcdghklIJ') + // assert.equal(s.slice(5, -5), 'FcdghklI') + // assert.equal(s.slice(6, -6), 'gh') + // }) -// it('supports characters moved inward', () => { -// const s = new MagicString('abCDefghijKLmn') + // it('supports characters moved inward', () => { + // const s = new MagicString('abCDefghijKLmn') -// s.move(2, 4, 6) -// s.move(10, 12, 8) -// assert.equal(s.toString(), 'abefCDghKLijmn') - -// assert.equal(s.slice(1, -1), 'befCDghKLijm') -// assert.equal(s.slice(2, -2), 'CDghKL') -// assert.equal(s.slice(3, -3), 'DghK') -// assert.equal(s.slice(4, -4), 'efCDghKLij') -// assert.equal(s.slice(5, -5), 'fCDghKLi') -// assert.equal(s.slice(6, -6), 'gh') -// }) + // s.move(2, 4, 6) + // s.move(10, 12, 8) + // assert.equal(s.toString(), 'abefCDghKLijmn') -// it('supports characters moved opposing', () => { -// const s = new MagicString('abCDefghIJkl') - -// s.move(2, 4, 8) -// s.move(8, 10, 4) -// assert.equal(s.toString(), 'abIJefghCDkl') - -// assert.equal(s.slice(1, -1), 'bIJefghCDk') -// assert.equal(s.slice(2, -2), '') -// assert.equal(s.slice(3, -3), '') -// assert.equal(s.slice(-3, 3), 'JefghC') -// assert.equal(s.slice(4, -4), 'efgh') -// assert.equal(s.slice(0, 3), 'abIJefghC') -// assert.equal(s.slice(3), 'Dkl') -// assert.equal(s.slice(0, -3), 'abI') -// assert.equal(s.slice(-3), 'JefghCDkl') -// }) + // assert.equal(s.slice(1, -1), 'befCDghKLijm') + // assert.equal(s.slice(2, -2), 'CDghKL') + // assert.equal(s.slice(3, -3), 'DghK') + // assert.equal(s.slice(4, -4), 'efCDghKLij') + // assert.equal(s.slice(5, -5), 'fCDghKLi') + // assert.equal(s.slice(6, -6), 'gh') + // }) + + // it('supports characters moved opposing', () => { + // const s = new MagicString('abCDefghIJkl') + + // s.move(2, 4, 8) + // s.move(8, 10, 4) + // assert.equal(s.toString(), 'abIJefghCDkl') + + // assert.equal(s.slice(1, -1), 'bIJefghCDk') + // assert.equal(s.slice(2, -2), '') + // assert.equal(s.slice(3, -3), '') + // assert.equal(s.slice(-3, 3), 'JefghC') + // assert.equal(s.slice(4, -4), 'efgh') + // assert.equal(s.slice(0, 3), 'abIJefghC') + // assert.equal(s.slice(3), 'Dkl') + // assert.equal(s.slice(0, -3), 'abI') + // assert.equal(s.slice(-3), 'JefghCDkl') + // }) -// it('errors if replaced characters are used as slice anchors', () => { -// const s = new MagicString('abcdef') -// s.overwrite(2, 4, 'CD') + // it('errors if replaced characters are used as slice anchors', () => { + // const s = new MagicString('abcdef') + // s.overwrite(2, 4, 'CD') -// assert.throws(() => s.slice(2, 3), /slice end anchor/) + // assert.throws(() => s.slice(2, 3), /slice end anchor/) -// assert.throws(() => s.slice(3, 4), /slice start anchor/) + // assert.throws(() => s.slice(3, 4), /slice start anchor/) -// assert.throws(() => s.slice(3, 5), /slice start anchor/) + // assert.throws(() => s.slice(3, 5), /slice start anchor/) -// assert.equal(s.slice(1, 5), 'bCDe') -// }) + // assert.equal(s.slice(1, 5), 'bCDe') + // }) -// it('does not error if slice is after removed characters', () => { -// const s = new MagicString('abcdef') + it('does not error if slice is after removed characters', () => { + const s = new MagicString('abcdef') -// s.remove(0, 2) + s.remove(0, 2) -// assert.equal(s.slice(2, 4), 'cd') -// }) -// }) + assert.equal(s.slice(2, 4), 'cd') + }) +}) // describe('snip', () => { // it('should return a clone with content outside `start` and `end` removed', () => { From 406913283b24afc14416f26f358e2d8e3ca978a5 Mon Sep 17 00:00:00 2001 From: faga Date: Fri, 17 Mar 2023 20:44:10 +0800 Subject: [PATCH 3/3] refactor: slice --- core/src/chunk.rs | 4 --- core/src/magic_string.rs | 72 +++++++++++++++++++++++++++++----------- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/core/src/chunk.rs b/core/src/chunk.rs index cd2e46f..6dc5a6c 100644 --- a/core/src/chunk.rs +++ b/core/src/chunk.rs @@ -16,8 +16,6 @@ pub struct Chunk { pub next: Option>>, pub prev: Option>>, - - pub edited: bool, } impl Chunk { @@ -33,8 +31,6 @@ impl Chunk { next: None, prev: None, - - edited: false, } } diff --git a/core/src/magic_string.rs b/core/src/magic_string.rs index b3f2ee9..eddadc6 100644 --- a/core/src/magic_string.rs +++ b/core/src/magic_string.rs @@ -301,7 +301,6 @@ impl MagicString { } chunk.borrow_mut().content = String::default(); - chunk.borrow_mut().edited = true; if !content_only { chunk.borrow_mut().intro = String::default(); chunk.borrow_mut().outro = String::default(); @@ -528,14 +527,11 @@ impl MagicString { /// ## Slice /// Get a slice of the modified string. - /// /// Example: /// ``` /// use magic_string::MagicString; - /// /// let mut s = MagicString::new("abcdefghijkl"); - /// assert_eq!(s.slice(1,5)?, "bcde"); - /// assert_eq!(s.slice(1, -1)?, "bcdefghijk"); + /// /// /// ``` /// @@ -553,28 +549,64 @@ impl MagicString { )); } - self._split_at_index(start)?; - self._split_at_index(end)?; + let mut result = String::new(); + let mut chunk = Some(Rc::clone(&self.first_chunk)); + while let Some(c) = chunk.clone() { + if c.borrow().start > start || c.borrow().end <= start { + chunk = c.borrow().clone().next; + } else { + break; + } + } + if let Some(c) = chunk.clone() { + if c.borrow().is_content_edited() && c.borrow().start != start { + return Err(Error::new_with_reason( + MagicStringErrorType::MagicStringUnknownError, + "Cannot move a selection inside itself", + )); + } + } + let start_chunk = chunk.clone().unwrap(); + Chunk::try_each_next(Rc::clone(&chunk.unwrap()), |chunk| { + let str: &str; - let first = self.chunk_by_start.get(&start); - let last = self.chunk_by_end.get(&end); + if chunk.borrow().intro.len() != 0 && (start_chunk != chunk || chunk.borrow().start == start) + { + result.push_str(chunk.borrow().intro.as_str()); + }; + + let contain_end = chunk.borrow().end >= end; + + let slice_start = if chunk.borrow().start < start { + start - chunk.borrow().start + } else { + 0 + }; + let slice_end = if contain_end { + chunk.borrow().content.len() as u32 + end - chunk.borrow().end + } else { + chunk.borrow().content.len() as u32 + }; - let mut str = String::new(); + let chunk_str = chunk.borrow().content.clone(); - if first.is_some() && last.is_some() { - if first.clone().unwrap().borrow().edited { + if contain_end && chunk.borrow().is_content_edited() && chunk.borrow().end != end { return Err(Error::new_with_reason( - MagicStringErrorType::MagicStringOutOfRangeError, - "Cannot slice a string that has been edited.", + MagicStringErrorType::MagicStringUnknownError, + "Cannot use replaced character ${end} as slice end anchor.", )); } - Chunk::try_each_next(Rc::clone(first.unwrap()), |chunk| { - str.push_str(chunk.borrow().to_string().as_str()); - Ok(chunk == Rc::clone(last.unwrap())) - })?; - } - Ok(str) + str = &chunk_str.as_str()[slice_start as usize..slice_end as usize]; + result.push_str(str); + if chunk.borrow().outro.len() != 0 && (!contain_end || chunk.borrow().end == end) { + result.push_str(chunk.borrow().outro.as_str()) + } + + Ok(chunk.borrow().end >= end) + })?; + + Ok(result) } /// ## Is empty ///