@@ -229,6 +229,199 @@ pub fn apply_edits(code: &str, mut edits: Vec<Edit>) -> String {
229229 result
230230}
231231
232+ /// Apply edits to source code and generate a source map.
233+ ///
234+ /// Uses the same edit-application algorithm as `apply_edits`, then generates
235+ /// a source map by finding where unchanged source segments appear in the
236+ /// actual output — guaranteeing the sourcemap is consistent with the output
237+ /// regardless of edit ordering.
238+ pub fn apply_edits_with_sourcemap (
239+ code : & str ,
240+ edits : Vec < Edit > ,
241+ filename : & str ,
242+ ) -> ( String , Option < String > ) {
243+ // Generate the output using the existing algorithm
244+ let output = apply_edits ( code, edits. clone ( ) ) ;
245+
246+ // Generate sourcemap by finding unchanged source segments in the actual output
247+ let map = generate_sourcemap_from_edits ( code, & output, edits, filename) ;
248+ ( output, Some ( map) )
249+ }
250+
251+ /// Generate a source map by finding unchanged source segments in the actual output.
252+ ///
253+ /// Instead of independently modeling how edits transform positions (which could
254+ /// diverge from `apply_edits`'s reverse-order mutating algorithm), this function:
255+ /// 1. Computes which source byte ranges are untouched by any edit
256+ /// 2. Locates each unchanged segment in the actual output string
257+ /// 3. Generates identity mappings for those segments
258+ ///
259+ /// This guarantees the sourcemap is always consistent with the actual output.
260+ fn generate_sourcemap_from_edits (
261+ source : & str ,
262+ output : & str ,
263+ edits : Vec < Edit > ,
264+ filename : & str ,
265+ ) -> String {
266+ let mut builder = oxc_sourcemap:: SourceMapBuilder :: default ( ) ;
267+ builder. set_source_and_content ( filename, source) ;
268+
269+ if edits. is_empty ( ) {
270+ // Identity mapping — every line maps 1:1
271+ add_line_mappings_for_segment ( & mut builder, source, 0 , 0 , 0 , 0 ) ;
272+ return builder. into_sourcemap ( ) . to_json_string ( ) ;
273+ }
274+
275+ // 1. Collect all edit boundary positions.
276+ // Every edit start/end position is a point where the output may differ from
277+ // the source. We need to split unchanged ranges at ALL edit positions —
278+ // including pure insertions (start == end) — because insertions embed new
279+ // text within what would otherwise be a contiguous source segment, breaking
280+ // `find(segment)` in step 3.
281+ let code_len = source. len ( ) as u32 ;
282+ let mut boundary_points: Vec < u32 > = Vec :: new ( ) ;
283+ let mut deleted_ranges: Vec < ( u32 , u32 ) > = Vec :: new ( ) ;
284+
285+ for edit in & edits {
286+ if edit. start > code_len || edit. end > code_len || edit. start > edit. end {
287+ continue ;
288+ }
289+ boundary_points. push ( edit. start ) ;
290+ boundary_points. push ( edit. end ) ;
291+ if edit. start < edit. end {
292+ deleted_ranges. push ( ( edit. start , edit. end ) ) ;
293+ }
294+ }
295+
296+ boundary_points. push ( 0 ) ;
297+ boundary_points. push ( code_len) ;
298+ boundary_points. sort_unstable ( ) ;
299+ boundary_points. dedup ( ) ;
300+
301+ // Merge overlapping deleted ranges for quick overlap checks
302+ deleted_ranges. sort_by_key ( |r| r. 0 ) ;
303+ let mut merged_deleted: Vec < ( u32 , u32 ) > = Vec :: new ( ) ;
304+ for ( s, e) in deleted_ranges {
305+ if let Some ( last) = merged_deleted. last_mut ( ) {
306+ if s <= last. 1 {
307+ last. 1 = last. 1 . max ( e) ;
308+ continue ;
309+ }
310+ }
311+ merged_deleted. push ( ( s, e) ) ;
312+ }
313+
314+ // 2. Compute unchanged source sub-ranges.
315+ // A sub-range [boundary[i], boundary[i+1]) is unchanged if it doesn't
316+ // overlap with any deletion range.
317+ let mut unchanged: Vec < ( u32 , u32 ) > = Vec :: new ( ) ;
318+ for window in boundary_points. windows ( 2 ) {
319+ let ( start, end) = ( window[ 0 ] , window[ 1 ] ) ;
320+ if start >= end {
321+ continue ;
322+ }
323+ // Check if this sub-range overlaps with any merged deletion
324+ let overlaps = merged_deleted. iter ( ) . any ( |( del_s, del_e) | start < * del_e && end > * del_s) ;
325+ if !overlaps {
326+ unchanged. push ( ( start, end) ) ;
327+ }
328+ }
329+
330+ // 3. Compute the output byte offset for each unchanged segment and generate mappings.
331+ // Instead of using string search (which can false-match replacement text for
332+ // short segments like `}`), we compute the exact output position using the
333+ // edit shift formula:
334+ // output_pos(S) = S + Σ (replacement.len() - (end - start))
335+ // for all edits where end <= S
336+ // This is exact for non-overlapping edits.
337+
338+ // Precompute edit shifts sorted by end position for efficient prefix-sum lookup
339+ let mut edit_shifts: Vec < ( u32 , i64 ) > = edits
340+ . iter ( )
341+ . filter ( |e| e. start <= code_len && e. end <= code_len && e. start <= e. end )
342+ . map ( |e| ( e. end , e. replacement . len ( ) as i64 - ( e. end as i64 - e. start as i64 ) ) )
343+ . collect ( ) ;
344+ edit_shifts. sort_by_key ( |( end, _) | * end) ;
345+
346+ for ( src_start, src_end) in & unchanged {
347+ let segment = & source[ * src_start as usize ..* src_end as usize ] ;
348+ if segment. is_empty ( ) {
349+ continue ;
350+ }
351+ // Compute output byte offset: src_start + net shift from all edits ending at or before src_start
352+ let net_shift: i64 = edit_shifts
353+ . iter ( )
354+ . take_while ( |( end, _) | * end <= * src_start)
355+ . map ( |( _, shift) | shift)
356+ . sum ( ) ;
357+ let output_byte_pos = ( * src_start as i64 + net_shift) as usize ;
358+
359+ debug_assert ! (
360+ output_byte_pos + segment. len( ) <= output. len( )
361+ && & output[ output_byte_pos..output_byte_pos + segment. len( ) ] == segment,
362+ "Sourcemap: computed output position {output_byte_pos} does not match \
363+ segment {:?} (src {}..{})",
364+ & segment[ ..segment. len( ) . min( 20 ) ] ,
365+ src_start,
366+ src_end,
367+ ) ;
368+
369+ let ( src_line, src_col) = byte_offset_to_line_col_utf16 ( source, * src_start as usize ) ;
370+ let ( out_line, out_col) = byte_offset_to_line_col_utf16 ( output, output_byte_pos) ;
371+ add_line_mappings_for_segment ( & mut builder, segment, out_line, out_col, src_line, src_col) ;
372+ }
373+
374+ builder. into_sourcemap ( ) . to_json_string ( )
375+ }
376+
377+ /// Compute line and column (UTF-16 code units) for a byte offset in a string.
378+ ///
379+ /// Source map columns must be in UTF-16 code units per the spec and `oxc_sourcemap`
380+ /// convention. For ASCII this equals byte offset; for multi-byte characters
381+ /// (e.g., `ɵ` U+0275 = 2 UTF-8 bytes but 1 UTF-16 code unit) the values differ.
382+ fn byte_offset_to_line_col_utf16 ( source : & str , offset : usize ) -> ( u32 , u32 ) {
383+ let mut line: u32 = 0 ;
384+ let mut col: u32 = 0 ;
385+ for ( i, ch) in source. char_indices ( ) {
386+ if i >= offset {
387+ break ;
388+ }
389+ if ch == '\n' {
390+ line += 1 ;
391+ col = 0 ;
392+ } else {
393+ col += ch. len_utf16 ( ) as u32 ;
394+ }
395+ }
396+ ( line, col)
397+ }
398+
399+ /// Add source map mappings for an unchanged segment of source code.
400+ ///
401+ /// Adds a mapping at the start of the segment and at the beginning of each new line.
402+ fn add_line_mappings_for_segment (
403+ builder : & mut oxc_sourcemap:: SourceMapBuilder ,
404+ segment : & str ,
405+ mut out_line : u32 ,
406+ mut out_col : u32 ,
407+ mut src_line : u32 ,
408+ mut src_col : u32 ,
409+ ) {
410+ // Add mapping at the start of this segment
411+ builder. add_token ( out_line, out_col, src_line, src_col, Some ( 0 ) , None ) ;
412+
413+ for ch in segment. chars ( ) {
414+ if ch == '\n' {
415+ out_line += 1 ;
416+ out_col = 0 ;
417+ src_line += 1 ;
418+ src_col = 0 ;
419+ // Add mapping at the start of each new line
420+ builder. add_token ( out_line, out_col, src_line, src_col, Some ( 0 ) , None ) ;
421+ }
422+ }
423+ }
424+
232425#[ cfg( test) ]
233426mod tests {
234427 use super :: * ;
0 commit comments