Skip to main content

string_wizard/magic_string/
update.rs

1use crate::{CowStr, MagicString, chunk::EditOptions};
2
3#[derive(Debug, Default, Clone)]
4pub struct UpdateOptions {
5  /// `true` will store the original content in the `name` field of the generated sourcemap.
6  pub keep_original: bool,
7
8  /// `true` will clear the `intro` and `outro` for the corresponding range.
9  pub overwrite: bool,
10}
11
12impl<'text> MagicString<'text> {
13  /// A shorthand for `update_with(start, end, content, Default::default())`;
14  pub fn update(
15    &mut self,
16    start: u32,
17    end: u32,
18    content: impl Into<CowStr<'text>>,
19  ) -> Result<&mut Self, String> {
20    self.update_with(start, end, content, Default::default())
21  }
22
23  pub fn update_with(
24    &mut self,
25    start: u32,
26    end: u32,
27    content: impl Into<CowStr<'text>>,
28    opts: UpdateOptions,
29  ) -> Result<&mut Self, String> {
30    self.inner_update_with(start, end, content.into(), opts, true)
31  }
32
33  // --- private
34
35  pub(super) fn inner_update_with(
36    &mut self,
37    start: u32,
38    end: u32,
39    content: CowStr<'text>,
40    opts: UpdateOptions,
41    error_if_start_equal_end: bool,
42  ) -> Result<&mut Self, String> {
43    if error_if_start_equal_end && start == end {
44      return Err(
45        "Cannot overwrite a zero-length range – use appendLeft or prependRight instead".to_string(),
46      );
47    }
48    if start >= end {
49      return Err(format!("end must be greater than start, got start: {start}, end: {end}"));
50    }
51    self.split_at(start)?;
52    self.split_at(end)?;
53
54    // Record the *whole* replaced range, not the start chunk's span: `start..end` may cross a
55    // boundary left by an earlier split, in which case the start chunk only covers part of it.
56    // Matches magic-string, which stores `original.slice(start, end)` here.
57    if opts.keep_original {
58      self.store_name(start, end);
59    }
60
61    let start_idx = self.chunk_by_start.get(&start).copied().unwrap();
62    let end_idx = self.chunk_by_end.get(&end).copied().unwrap();
63
64    if start_idx != end_idx {
65      // When the update range spans multiple chunks, we need to:
66      // 1. Detect if any chunk within the range has been moved (via `move()`). A moved
67      //    chunk's linked-list successor (`chunk.next`) will differ from its positional
68      //    successor (`chunk_by_start[chunk.end]`). Overwriting across such a boundary
69      //    is invalid because the chunks are no longer contiguous in the output.
70      // 2. Clear each interior/end chunk's content (set to "").
71      //
72      // This mirrors the JS magic-string `update()` implementation:
73      //   https://github.com/Rich-Harris/magic-string/blob/410fd4d/src/MagicString.js#L420-L428
74      let mut chunk_idx = start_idx;
75      loop {
76        let next_in_list = self.chunks[chunk_idx].next;
77        let chunk_end = self.chunks[chunk_idx].end();
78        let next_by_position = self.chunk_by_start.get(&chunk_end).copied();
79
80        if next_in_list != next_by_position {
81          return Err("Cannot overwrite across a split point".to_string());
82        }
83
84        // Both are `None` when the walk runs off the end of the list without reaching
85        // `end_idx`, i.e. the range is not contiguous in the output — same failure as above.
86        let Some(next_idx) = next_in_list else {
87          return Err("Cannot overwrite across a split point".to_string());
88        };
89        chunk_idx = next_idx;
90        // Interior chunks always clear intro/outro (`Default` has `overwrite: true`),
91        // matching JS magic-string where `chunk.edit('', false)` passes
92        // `contentOnly=undefined` (falsy), so intro/outro are always cleared.
93        self.chunks[chunk_idx].edit("".into(), Default::default());
94
95        if chunk_idx == end_idx {
96          break;
97        }
98      }
99    }
100
101    // Edit the start chunk last — only this chunk receives the replacement content
102    // and respects the caller's `overwrite` option (JS `contentOnly`).
103    self.chunks[start_idx]
104      .edit(content, EditOptions { overwrite: opts.overwrite, store_name: opts.keep_original });
105    Ok(self)
106  }
107}