Skip to content

Commit 738c04b

Browse files
authored
feat: support relocate (#6)
1 parent d9cb1e8 commit 738c04b

File tree

6 files changed

+244
-34
lines changed

6 files changed

+244
-34
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "string_wizard"
3-
version = "0.0.7"
3+
version = "0.0.8"
44
edition = "2021"
55
license = "MIT"
66
description = "manipulate string like wizards"

src/chunk.rs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ pub struct Chunk<'str> {
3232
pub outro: VecDeque<CowStr<'str>>,
3333
pub span: Span,
3434
pub edited_content: Option<CowStr<'str>>,
35-
pub(crate) next: Option<ChunkIdx>,
36-
pub store_name: bool,
35+
pub next: Option<ChunkIdx>,
36+
pub prev: Option<ChunkIdx>,
37+
pub keep_in_mappings: bool,
3738
}
3839

3940
impl<'s> Chunk<'s> {
@@ -76,20 +77,26 @@ impl<'str> Chunk<'str> {
7677
}
7778

7879
pub fn split<'a>(&'a mut self, text_index: TextSize) -> Chunk<'str> {
79-
debug_assert!(text_index > self.start());
80-
debug_assert!(text_index < self.end());
80+
if !(text_index > self.start() && text_index < self.end()) {
81+
panic!("Cannot split chunk at {text_index} between {:?}", self.span);
82+
}
8183
if self.edited_content.is_some() {
8284
panic!("Cannot split a chunk that has already been edited")
8385
}
84-
let first_slice_span = Span(self.start(), text_index);
85-
let last_slice_span = Span(text_index, self.end());
86-
let mut new_chunk = Chunk::new(last_slice_span);
86+
let first_half_slice = Span(self.start(), text_index);
87+
let second_half_slice = Span(text_index, self.end());
88+
let mut new_chunk = Chunk::new(second_half_slice);
8789
if self.is_edited() {
88-
new_chunk.edit("".into(), Default::default());
90+
new_chunk.edit(
91+
"".into(),
92+
EditOptions {
93+
store_name: self.keep_in_mappings,
94+
overwrite: false,
95+
},
96+
);
8997
}
9098
std::mem::swap(&mut new_chunk.outro, &mut self.outro);
91-
self.span = first_slice_span;
92-
new_chunk.next = self.next;
99+
self.span = first_half_slice;
93100
new_chunk
94101
}
95102

@@ -112,7 +119,7 @@ impl<'str> Chunk<'str> {
112119
self.intro.clear();
113120
self.outro.clear();
114121
}
115-
self.store_name = opts.store_name;
122+
self.keep_in_mappings = opts.store_name;
116123
self.edited_content = Some(content);
117124
}
118125

src/magic_string/mod.rs

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -200,41 +200,52 @@ impl<'text> MagicString<'text> {
200200
///
201201
/// Chunk{span: (0, 3)} => "abc"
202202
/// Chunk{span: (3, 7)} => "defg"
203-
fn split_at(&mut self, text_index: u32) {
204-
if text_index == 0 || self.chunk_by_end.contains_key(&text_index) {
203+
fn split_at(&mut self, at_index: u32) {
204+
if at_index == 0
205+
|| at_index >= self.source.len()
206+
|| self.chunk_by_end.contains_key(&at_index)
207+
{
205208
return;
206209
}
207210

208-
let (mut target, mut target_idx, search_right) =
209-
if (self.source.len() - text_index) > text_index {
211+
let (mut candidate, mut candidate_idx, search_right) =
212+
if (self.source.len() - at_index) > at_index {
210213
(self.first_chunk(), self.first_chunk_idx, true)
211214
} else {
212215
(self.last_chunk(), self.last_chunk_idx, false)
213216
};
214217

215-
while !target.contains(text_index) {
218+
while !candidate.contains(at_index) {
216219
let next_idx = if search_right {
217-
self.chunk_by_start.get(&target.end()).unwrap()
220+
self.chunk_by_start[&candidate.end()]
218221
} else {
219-
self.chunk_by_end.get(&target.start()).unwrap()
222+
self.chunk_by_end[&candidate.start()]
220223
};
221-
target = &self.chunks[*next_idx];
222-
target_idx = *next_idx;
224+
candidate = &self.chunks[next_idx];
225+
candidate_idx = next_idx;
223226
}
224227

225-
let chunk_contains_index = &mut self.chunks[target_idx];
226-
let new_chunk = chunk_contains_index.split(text_index);
227-
self.chunk_by_end.insert(text_index, target_idx);
228-
let new_chunk_end = new_chunk.end();
229-
let new_chunk_idx = self.chunks.push(new_chunk);
230-
self.chunk_by_start.insert(text_index, new_chunk_idx);
231-
self.chunk_by_end.insert(new_chunk_end, new_chunk_idx);
232-
233-
let chunk_contains_index = &mut self.chunks[target_idx];
234-
if target_idx == self.last_chunk_idx {
235-
self.last_chunk_idx = new_chunk_idx
228+
let second_half_chunk = self.chunks[candidate_idx].split(at_index);
229+
let second_half_span = second_half_chunk.span;
230+
let second_half_idx = self.chunks.push(second_half_chunk);
231+
let first_half_idx = candidate_idx;
232+
233+
// Update the chunk_by_start/end maps
234+
self.chunk_by_end.insert(at_index, first_half_idx);
235+
self.chunk_by_start.insert(at_index, second_half_idx);
236+
self.chunk_by_end
237+
.insert(second_half_span.end(), second_half_idx);
238+
239+
// Make sure the new chunk and the old chunk have correct next/prev pointers
240+
self.chunks[second_half_idx].next = self.chunks[first_half_idx].next;
241+
if let Some(second_half_next_idx) = self.chunks[second_half_idx].next {
242+
self.chunks[second_half_next_idx].prev = Some(second_half_idx);
243+
}
244+
self.chunks[second_half_idx].prev = Some(first_half_idx);
245+
self.chunks[first_half_idx].next = Some(second_half_idx);
246+
if first_half_idx == self.last_chunk_idx {
247+
self.last_chunk_idx = second_half_idx
236248
}
237-
chunk_contains_index.next = Some(new_chunk_idx);
238249
}
239250

240251
fn by_start_mut(&mut self, text_index: impl AssertIntoU32) -> Option<&mut Chunk<'text>> {

src/magic_string/mutation.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,81 @@ impl<'text> MagicString<'text> {
5252
self
5353
}
5454

55+
/// Moves the characters from start and end to index. Returns this.
56+
// `move` is reserved keyword in rust, so we use `relocate` instead.
57+
pub fn relocate(
58+
&mut self,
59+
start: impl AssertIntoU32,
60+
end: impl AssertIntoU32,
61+
to: impl AssertIntoU32,
62+
) -> &mut Self {
63+
let start = start.assert_into_u32();
64+
let end = end.assert_into_u32();
65+
let to = to.assert_into_u32();
66+
67+
self.split_at(start);
68+
self.split_at(end);
69+
self.split_at(to);
70+
71+
let first_idx = self.chunk_by_start[&start];
72+
let last_idx = self.chunk_by_end[&end];
73+
74+
let old_left_idx = self.chunks[first_idx].prev;
75+
let old_right_idx = self.chunks[last_idx].next;
76+
77+
let new_right_idx = self.chunk_by_start.get(&to).copied();
78+
79+
// `new_right_idx` is `None` means that the `to` index is at the end of the string.
80+
// Moving chunks which contain the last chunk to the end is meaningless.
81+
if new_right_idx.is_none() && last_idx == self.last_chunk_idx {
82+
return self;
83+
}
84+
85+
let new_left_idx = new_right_idx
86+
.map(|idx| self.chunks[idx].prev)
87+
// If the `to` index is at the end of the string, then the `new_right_idx` will be `None`.
88+
// In this case, we want to use the last chunk as the left chunk to connect the relocated chunk.
89+
.unwrap_or(Some(self.last_chunk_idx));
90+
91+
// Adjust next/prev pointers, this remove the [start, end] range from the old position
92+
if let Some(old_left_idx) = old_left_idx {
93+
self.chunks[old_left_idx].next = old_right_idx;
94+
}
95+
if let Some(old_right_idx) = old_right_idx {
96+
self.chunks[old_right_idx].prev = old_left_idx;
97+
}
98+
99+
if let Some(new_left_idx) = new_left_idx {
100+
self.chunks[new_left_idx].next = Some(first_idx);
101+
}
102+
if let Some(new_right_idx) = new_right_idx {
103+
self.chunks[new_right_idx].prev = Some(last_idx);
104+
}
105+
106+
if self.chunks[first_idx].prev.is_none() {
107+
// If the `first_idx` is the first chunk, then we need to update the `first_chunk_idx`.
108+
self.first_chunk_idx = self.chunks[last_idx].next.unwrap();
109+
}
110+
if self.chunks[last_idx].next.is_none() {
111+
// If the `last_idx` is the last chunk, then we need to update the `last_chunk_idx`.
112+
self.last_chunk_idx = self.chunks[first_idx].prev.unwrap();
113+
self.chunks[last_idx].next = None;
114+
}
115+
116+
if new_left_idx.is_none() {
117+
self.first_chunk_idx = first_idx;
118+
}
119+
if new_right_idx.is_none() {
120+
self.last_chunk_idx = last_idx;
121+
}
122+
123+
self.chunks[first_idx].prev = new_left_idx;
124+
self.chunks[last_idx].next = new_right_idx;
125+
126+
127+
self
128+
}
129+
55130
// --- private
56131

57132
fn update_with_inner(

src/magic_string/source_map.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ impl<'s> MagicString<'s> {
2525
mappings.advance(frag);
2626
});
2727

28-
let name_idx = if chunk.store_name && chunk.is_edited() {
28+
let name_idx = if chunk.keep_in_mappings && chunk.is_edited() {
2929
let original_content = chunk.span.text(&self.source);
3030

3131
let idx = names

tests/magic_string.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,123 @@ mod overwrite {
150150
// }
151151
}
152152

153+
mod relocate {
154+
use super::*;
155+
156+
#[test]
157+
fn moves_content_from_the_start() {
158+
let mut s = MagicString::new("abcdefghijkl");
159+
s.relocate(0, 3, 6);
160+
assert_eq!(s.to_string(), "defabcghijkl");
161+
}
162+
163+
#[test]
164+
fn moves_content_to_the_start() {
165+
let mut s = MagicString::new("abcdefghijkl");
166+
s.relocate(3, 6, 0);
167+
assert_eq!(s.to_string(), "defabcghijkl");
168+
}
169+
170+
#[test]
171+
fn moves_content_from_the_end() {
172+
let mut s = MagicString::new("abcdefghijkl");
173+
s.relocate(9, 12, 6);
174+
assert_eq!(s.to_string(), "abcdefjklghi");
175+
}
176+
177+
#[test]
178+
fn moves_content_to_the_end() {
179+
let mut s = MagicString::new("abcdefghijkl");
180+
s.relocate(6, 9, 12);
181+
assert_eq!(s.to_string(), "abcdefjklghi");
182+
}
183+
184+
#[test]
185+
fn ignores_redundant_move() {
186+
let mut s = MagicString::new("abcdefghijkl");
187+
s.prepend_right(9, "X")
188+
.relocate(9, 12, 6)
189+
.append_left(12, "Y")
190+
// this is redundant – [6,9] is already after [9,12]
191+
.relocate(6, 9, 12);
192+
193+
assert_eq!(s.to_string(), "abcdefXjklYghi");
194+
}
195+
196+
#[test]
197+
fn moves_content_to_the_middle() {
198+
let mut s = MagicString::new("abcdefghijkl");
199+
s.relocate(3, 6, 9);
200+
assert_eq!(s.to_string(), "abcghidefjkl");
201+
}
202+
203+
#[test]
204+
fn handles_multiple_moves_of_the_same_snippet() {
205+
let mut s = MagicString::new("abcdefghijkl");
206+
207+
s.relocate(0, 3, 6);
208+
assert_eq!(s.to_string(), "defabcghijkl");
209+
210+
s.relocate(0, 3, 9);
211+
assert_eq!(s.to_string(), "defghiabcjkl");
212+
}
213+
214+
#[test]
215+
fn handles_moves_of_adjacent_snippets() {
216+
let mut s = MagicString::new("abcdefghijkl");
217+
218+
s.relocate(0, 2, 6);
219+
assert_eq!(s.to_string(), "cdefabghijkl");
220+
s.relocate(2, 4, 6);
221+
assert_eq!(s.to_string(), "efabcdghijkl");
222+
}
223+
224+
#[test]
225+
fn handles_moves_to_same_index() {
226+
let mut s = MagicString::new("abcdefghijkl");
227+
s.relocate(0, 2, 6).relocate(3, 5, 6);
228+
assert_eq!(s.to_string(), "cfabdeghijkl");
229+
}
230+
231+
#[test]
232+
#[should_panic]
233+
fn refuses_to_move_a_selection_to_inside_itself() {
234+
let mut s = MagicString::new("abcdefghijkl");
235+
s.relocate(3, 6, 3);
236+
}
237+
#[test]
238+
#[should_panic]
239+
fn refuses_to_move_a_selection_to_inside_itself2() {
240+
let mut s = MagicString::new("abcdefghijkl");
241+
s.relocate(3, 6, 4);
242+
}
243+
#[test]
244+
#[should_panic]
245+
fn refuses_to_move_a_selection_to_inside_itself3() {
246+
let mut s = MagicString::new("abcdefghijkl");
247+
s.relocate(3, 6, 6);
248+
}
249+
250+
#[test]
251+
fn allows_edits_of_moved_content() {
252+
let mut s1 = MagicString::new("abcdefghijkl");
253+
s1.relocate(3, 6, 9);
254+
s1.overwrite(3, 6, "DEF");
255+
assert_eq!(s1.to_string(), "abcghiDEFjkl");
256+
let mut s2 = MagicString::new("abcdefghijkl");
257+
s2.relocate(3, 6, 9);
258+
s2.overwrite(4, 5, "E");
259+
assert_eq!(s2.to_string(), "abcghidEfjkl");
260+
}
261+
262+
#[test]
263+
fn moves_content_inserted_at_end_of_range() {
264+
let mut s = MagicString::new("abcdefghijkl");
265+
s.append_left(6, "X").relocate(3, 6, 9);
266+
assert_eq!(s.to_string(), "abcghidefXjkl");
267+
}
268+
}
269+
153270
mod misc {
154271
use super::*;
155272

0 commit comments

Comments
 (0)