@@ -97,48 +97,36 @@ export abstract class BaseStringEdit<T extends BaseStringReplacement<T> = BaseSt
9797 let baseIdx = 0 ;
9898 let ourIdx = 0 ;
9999 let offset = 0 ;
100- let lastEndEx = - 1 ; // Track end of last added edit to ensure sorted/disjoint invariant
101100
102101 while ( ourIdx < this . replacements . length || baseIdx < base . replacements . length ) {
103102 // take the edit that starts first
104- const baseEdit = base . replacements [ baseIdx ] ;
105- const ourEdit = this . replacements [ ourIdx ] ;
103+ const baseEdit = base . replacements . at ( baseIdx ) ;
104+ const ourEdit = this . replacements . at ( ourIdx ) ;
106105
107106 if ( ! ourEdit ) {
108107 // We processed all our edits
109108 break ;
110109 } else if ( ! baseEdit ) {
111110 // no more edits from base
112111 const transformedRange = ourEdit . replaceRange . delta ( offset ) ;
113- // Check if the transformed edit would violate the sorted/disjoint invariant
114- if ( transformedRange . start < lastEndEx ) {
115- if ( noOverlap ) {
116- return undefined ;
117- }
118- ourIdx ++ ; // Skip this edit as it conflicts with a previously added edit
119- continue ;
120- }
121112 newEdits . push ( new StringReplacement ( transformedRange , ourEdit . newText ) ) ;
122- lastEndEx = transformedRange . endExclusive ;
123113 ourIdx ++ ;
124- } else if ( ourEdit . replaceRange . intersects ( baseEdit . replaceRange ) || areConcurrentInserts ( ourEdit . replaceRange , baseEdit . replaceRange ) ) {
114+ } else if (
115+ ourEdit . replaceRange . intersects ( baseEdit . replaceRange ) ||
116+ areConcurrentInserts ( ourEdit . replaceRange , baseEdit . replaceRange ) ||
117+ isInsertStrictlyInsideRange ( ourEdit . replaceRange , baseEdit . replaceRange ) ||
118+ isInsertStrictlyInsideRange ( baseEdit . replaceRange , ourEdit . replaceRange )
119+ ) {
125120 ourIdx ++ ; // Don't take our edit, as it is conflicting -> skip
126121 if ( noOverlap ) {
127122 return undefined ;
128123 }
129- } else if ( ourEdit . replaceRange . start < baseEdit . replaceRange . start ) {
130- // Our edit starts first
124+ } else if ( ourEdit . replaceRange . start < baseEdit . replaceRange . start ||
125+ ( ourEdit . replaceRange . isEmpty && ourEdit . replaceRange . start === baseEdit . replaceRange . start ) ) {
126+ // Our edit starts first, or is an insert at the start of base's range
131127 const transformedRange = ourEdit . replaceRange . delta ( offset ) ;
132128 // Check if the transformed edit would violate the sorted/disjoint invariant
133- if ( transformedRange . start < lastEndEx ) {
134- if ( noOverlap ) {
135- return undefined ;
136- }
137- ourIdx ++ ; // Skip this edit as it conflicts with a previously added edit
138- continue ;
139- }
140129 newEdits . push ( new StringReplacement ( transformedRange , ourEdit . newText ) ) ;
141- lastEndEx = transformedRange . endExclusive ;
142130 ourIdx ++ ;
143131 } else {
144132 baseIdx ++ ;
@@ -299,6 +287,25 @@ export abstract class BaseStringReplacement<T extends BaseStringReplacement<T> =
299287 * All these replacements are applied at once.
300288*/
301289export class StringEdit extends BaseStringEdit < StringReplacement , StringEdit > {
290+ /**
291+ * Parses an edit from its string representation.
292+ * E.g. [[2, 12) -> "fgh", [14, 20) -> "qrst", [22, 22) -> "de\n"]
293+ */
294+ public static parse ( toStringValue : string ) : StringEdit {
295+ const replacements : StringReplacement [ ] = [ ] ;
296+ const regex = / \[ ( \d + ) , \s * ( \d + ) \) \s * - > \s * " ( [ ^ " ] * ) " / g;
297+ let match ;
298+
299+ while ( ( match = regex . exec ( toStringValue ) ) !== null ) {
300+ const start = parseInt ( match [ 1 ] , 10 ) ;
301+ const endEx = parseInt ( match [ 2 ] , 10 ) ;
302+ const text = match [ 3 ] . replace ( / \\ n / g, '\n' ) . replace ( / \\ r / g, '\r' ) . replace ( / \\ \\ / g, '\\' ) ;
303+ replacements . push ( new StringReplacement ( new OffsetRange ( start , endEx ) , text ) ) ;
304+ }
305+
306+ return new StringEdit ( replacements ) ;
307+ }
308+
302309 public static readonly empty = new StringEdit ( [ ] ) ;
303310
304311 public static create ( replacements : readonly StringReplacement [ ] ) : StringEdit {
@@ -596,3 +603,11 @@ export class AnnotatedStringReplacement<T extends IEditData<T>> extends BaseStri
596603function areConcurrentInserts ( r1 : OffsetRange , r2 : OffsetRange ) : boolean {
597604 return r1 . isEmpty && r2 . isEmpty && r1 . start === r2 . start ;
598605}
606+
607+ /**
608+ * Returns true if `insert` is an empty range (insert) strictly inside `range`.
609+ * For example, insert at position 5 is inside [3, 7) but not inside [5, 7) or [3, 5).
610+ */
611+ function isInsertStrictlyInsideRange ( insert : OffsetRange , range : OffsetRange ) : boolean {
612+ return insert . isEmpty && range . start < insert . start && insert . start < range . endExclusive ;
613+ }
0 commit comments