stringEdit.ts ×63

Frontier kind: Code frontier

unlabeled · c_8b8dac34621c

3139 tests · 6202 LOC · 35 files · introduces 0 tests · 376 LOC · 2 files

Introduces — evidence that enters the hierarchy at this concept

Code
93 ranges376 lines · 2 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
1012 ranges6202 lines · 35 files · Browse complete extent
All tests (intent)
3139 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

2 files ranked by introduced lines: 376 introduced LOC across 93 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/editor/common/core/edits/stringEdit.ts 232 introduced LOC · 63 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- stringEdit.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { commonPrefixLength, commonSuffixLength } from '../../../../base/common/strings.js';
7 > import { OffsetRange } from '../ranges/offsetRange.js';
8 > import { StringText } from '../text/abstractText.js';
9 > import { BaseEdit, BaseReplacement } from './edit.js';
10 >
11 >
12 > // eslint-disable-next-line @typescript-eslint/no-explicit-any
13 > export abstract class BaseStringEdit<T extends BaseStringReplacement<T> = BaseStringReplacement<any>, TEdit extends BaseStringEdit<T, TEdit> = BaseStringEdit<any, any>> extends BaseEdit<T, TEdit> {
14 > get TReplacement(): T {
15 throw new Error('TReplacement is not defined for BaseStringEdit');
16 }
18 > public static composeOrUndefined<T extends BaseStringEdit>(edits: readonly T[]): T | undefined {
19 if (edits.length === 0) {
20 return undefined;
27 return result;
28 }
30 > /**
31 > * r := trySwap(e1, e2);
32 > * e1.compose(e2) === r.e1.compose(r.e2)
33 > */
34 > public static trySwap(e1: BaseStringEdit, e2: BaseStringEdit): { e1: StringEdit; e2: StringEdit } | undefined {
35 // TODO make this more efficient
36 const e1Inv = e1.inverseOnSlice((start, endEx) => ' '.repeat(endEx - start));
47 return { e1: e1_, e2: e2_ };
48 }
50 > public apply(base: string): string {
51 const resultText: string[] = [];
52 let pos = 0;
59 return resultText.join('');
60 }
62 >
63 > /**
64 > * Creates an edit that reverts this edit.
65 > */
66 > public inverseOnSlice(getOriginalSlice: (start: number, endEx: number) => string): StringEdit {
67 const edits: StringReplacement[] = [];
68 let offset = 0;
76 return new StringEdit(edits);
77 }
79 > /**
80 > * Creates an edit that reverts this edit.
81 > */
82 > public inverse(original: string): StringEdit {
83 return this.inverseOnSlice((start, endEx) => original.substring(start, endEx));
84 }
86 > public rebaseSkipConflicting(base: StringEdit): StringEdit {
87 return this._tryRebase(base, false)!;
88 }
90 > public tryRebase(base: StringEdit): StringEdit | undefined {
91 return this._tryRebase(base, true);
92 }
94 > private _tryRebase(base: StringEdit, noOverlap: boolean): StringEdit | undefined {
95 const newEdits: StringReplacement[] = [];
96
137 return new StringEdit(newEdits);
138 }
140 > public toJson(): ISerializedStringEdit {
141 return this.replacements.map(e => e.toJson());
142 }
144 > public isNeutralOn(text: string): boolean {
145 return this.replacements.every(e => e.isNeutralOn(text));
146 }
148 > public removeCommonSuffixPrefix(originalText: string): StringEdit {
149 const edits: StringReplacement[] = [];
150 for (const e of this.replacements) {
156 return new StringEdit(edits);
157 }
159 > public normalizeEOL(eol: '\r\n' | '\n'): StringEdit {
160 return new StringEdit(this.replacements.map(edit => edit.normalizeEOL(eol)));
161 }
163 > /**
164 > * If `e1.apply(source) === e2.apply(source)`, then `e1.normalizeOnSource(source).equals(e2.normalizeOnSource(source))`.
165 > */
166 > public normalizeOnSource(source: string): StringEdit {
167 const result = this.apply(source);
168
174 return e.toEdit();
175 }
177 > public removeCommonSuffixAndPrefix(source: string): TEdit {
178 return this._createNew(this.replacements.map(e => e.removeCommonSuffixAndPrefix(source))).normalize();
179 }
181 > public applyOnText(docContents: StringText): StringText {
182 return new StringText(this.apply(docContents.value));
183 }
185 > public mapData<TData extends IEditData<TData>>(f: (replacement: T) => TData): AnnotatedStringEdit<TData> {
186 return new AnnotatedStringEdit(
187 this.replacements.map(e => new AnnotatedStringReplacement(
192 );
193 }
194 > } stringEdit.ts
195 >
196 > // eslint-disable-next-line @typescript-eslint/no-explicit-any
197 > export abstract class BaseStringReplacement<T extends BaseStringReplacement<T> = BaseStringReplacement<any>> extends BaseReplacement<T> {
198 > constructor(
199 range: OffsetRange,
200 public readonly newText: string
202 super(range);
203 }
205 > getNewLength(): number { return this.newText.length; }
206 >
207 > override toString(): string {
208 return `${this.replaceRange} -> ${JSON.stringify(this.newText)}`;
209 }
211 > replace(str: string): string {
212 return str.substring(0, this.replaceRange.start) + this.newText + str.substring(this.replaceRange.endExclusive);
213 }
215 > /**
216 > * Checks if the edit would produce no changes when applied to the given text.
217 > */
218 > isNeutralOn(text: string): boolean {
219 return this.newText === text.substring(this.replaceRange.start, this.replaceRange.endExclusive);
220 }
222 > removeCommonSuffixPrefix(originalText: string): StringReplacement {
223 const oldText = originalText.substring(this.replaceRange.start, this.replaceRange.endExclusive);
224
238 return new StringReplacement(replaceRange, newText);
239 }
241 > normalizeEOL(eol: '\r\n' | '\n'): StringReplacement {
242 const newText = this.newText.replace(/\r\n|\n/g, eol);
243 return new StringReplacement(this.replaceRange, newText);
244 }
246 > public removeCommonSuffixAndPrefix(source: string): T {
247 return this.removeCommonSuffix(source).removeCommonPrefix(source);
248 }
250 > public removeCommonPrefix(source: string): T {
251 const oldText = this.replaceRange.substring(source);
252
258 return this.slice(this.replaceRange.deltaStart(prefixLen), new OffsetRange(prefixLen, this.newText.length));
259 }
261 > public removeCommonSuffix(source: string): T {
262 const oldText = this.replaceRange.substring(source);
263
268 return this.slice(this.replaceRange.deltaEnd(-suffixLen), new OffsetRange(0, this.newText.length - suffixLen));
269 }
271 > public toEdit(): StringEdit {
272 return new StringEdit([this]);
273 }
275 > public toJson(): ISerializedStringReplacement {
276 return ({
277 txt: this.newText,
280 });
281 }
282 > } stringEdit.ts
283 >
284 >
285 > /**
286 > * Represents a set of replacements to a string.
287 > * All these replacements are applied at once.
288 > */
289 > export 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;
306 return new StringEdit(replacements);
307 }
309 > public static readonly empty = new StringEdit([]);
310 >
311 > public static create(replacements: readonly StringReplacement[]): StringEdit {
312 return new StringEdit(replacements);
313 }
315 > public static single(replacement: StringReplacement): StringEdit {
316 return new StringEdit([replacement]);
317 }
319 > public static replace(range: OffsetRange, replacement: string): StringEdit {
320 return new StringEdit([new StringReplacement(range, replacement)]);
321 }
323 > public static insert(offset: number, replacement: string): StringEdit {
324 return new StringEdit([new StringReplacement(OffsetRange.emptyAt(offset), replacement)]);
325 }
327 > public static delete(range: OffsetRange): StringEdit {
328 return new StringEdit([new StringReplacement(range, '')]);
329 }
331 > public static fromJson(data: ISerializedStringEdit): StringEdit {
332 return new StringEdit(data.map(StringReplacement.fromJson));
333 }
335 > public static compose(edits: readonly StringEdit[]): StringEdit {
336 if (edits.length === 0) {
337 return StringEdit.empty;
343 return result;
344 }
346 > /**
347 > * The replacements are applied in order!
348 > * Equals `StringEdit.compose(replacements.map(r => r.toEdit()))`, but is much more performant.
349 > */
350 > public static composeSequentialReplacements(replacements: readonly StringReplacement[]): StringEdit {
351 let edit = StringEdit.empty;
352 let curEditReplacements: StringReplacement[] = []; // These are reverse sorted
367 return edit;
368 }
370 > constructor(replacements: readonly StringReplacement[]) {
371 > super(replacements);
372 > }
373 >
374 > protected override _createNew(replacements: readonly StringReplacement[]): StringEdit {
375 return new StringEdit(replacements);
376 }
377 > } stringEdit.ts
378 >
379 > /**
380 > * Warning: Be careful when changing this type, as it is used for serialization!
381 > */
382 > export type ISerializedStringEdit = ISerializedStringReplacement[];
383 >
384 > /**
385 > * Warning: Be careful when changing this type, as it is used for serialization!
386 > */
387 > export interface ISerializedStringReplacement {
388 > txt: string;
389 > pos: number;
390 > len: number;
391 > }
392 >
393 > export class StringReplacement extends BaseStringReplacement<StringReplacement> {
394 > public static insert(offset: number, text: string): StringReplacement {
395 return new StringReplacement(OffsetRange.emptyAt(offset), text);
396 }
398 > public static replace(range: OffsetRange, text: string): StringReplacement {
399 return new StringReplacement(range, text);
400 }
402 > public static delete(range: OffsetRange): StringReplacement {
403 return new StringReplacement(range, '');
404 }
406 > public static fromJson(data: ISerializedStringReplacement): StringReplacement {
407 return new StringReplacement(OffsetRange.ofStartAndLength(data.pos, data.len), data.txt);
408 }
410 > override equals(other: StringReplacement): boolean {
411 return this.replaceRange.equals(other.replaceRange) && this.newText === other.newText;
412 }
414 > override tryJoinTouching(other: StringReplacement): StringReplacement | undefined {
415 return new StringReplacement(this.replaceRange.joinRightTouching(other.replaceRange), this.newText + other.newText);
416 }
418 > override slice(range: OffsetRange, rangeInReplacement?: OffsetRange): StringReplacement {
419 return new StringReplacement(range, rangeInReplacement ? rangeInReplacement.substring(this.newText) : this.newText);
420 }
421 > } stringEdit.ts
422 >
423 > export function applyEditsToRanges(sortedRanges: OffsetRange[], edit: StringEdit): OffsetRange[] {
424 sortedRanges = sortedRanges.slice();
425
487 return result;
488 }
490 > /**
491 > * Represents data associated to a single edit, which survives certain edit operations.
492 > */
493 > export interface IEditData<T> {
494 > join(other: T): T | undefined;
495 > }
496 >
497 > export class VoidEditData implements IEditData<VoidEditData> {
498 > join(other: VoidEditData): VoidEditData | undefined {
499 return this;
500 }
501 > } stringEdit.ts
502 >
503 > /**
504 > * Represents a set of replacements to a string.
505 > * All these replacements are applied at once.
506 > */
507 > export class AnnotatedStringEdit<T extends IEditData<T>> extends BaseStringEdit<AnnotatedStringReplacement<T>, AnnotatedStringEdit<T>> {
508 > public static readonly empty = new AnnotatedStringEdit<never>([]);
509 >
510 > public static create<T extends IEditData<T>>(replacements: readonly AnnotatedStringReplacement<T>[]): AnnotatedStringEdit<T> {
511 return new AnnotatedStringEdit(replacements);
512 }
514 > public static single<T extends IEditData<T>>(replacement: AnnotatedStringReplacement<T>): AnnotatedStringEdit<T> {
515 return new AnnotatedStringEdit([replacement]);
516 }
518 > public static replace<T extends IEditData<T>>(range: OffsetRange, replacement: string, data: T): AnnotatedStringEdit<T> {
519 return new AnnotatedStringEdit([new AnnotatedStringReplacement(range, replacement, data)]);
520 }
522 > public static insert<T extends IEditData<T>>(offset: number, replacement: string, data: T): AnnotatedStringEdit<T> {
523 return new AnnotatedStringEdit([new AnnotatedStringReplacement(OffsetRange.emptyAt(offset), replacement, data)]);
524 }
526 > public static delete<T extends IEditData<T>>(range: OffsetRange, data: T): AnnotatedStringEdit<T> {
527 return new AnnotatedStringEdit([new AnnotatedStringReplacement(range, '', data)]);
528 }
530 > public static compose<T extends IEditData<T>>(edits: readonly AnnotatedStringEdit<T>[]): AnnotatedStringEdit<T> {
531 if (edits.length === 0) {
532 return AnnotatedStringEdit.empty;
538 return result;
539 }
541 > constructor(replacements: readonly AnnotatedStringReplacement<T>[]) {
542 > super(replacements);
543 > }
544 >
545 > protected override _createNew(replacements: readonly AnnotatedStringReplacement<T>[]): AnnotatedStringEdit<T> {
546 return new AnnotatedStringEdit<T>(replacements);
547 }
549 > public toStringEdit(filter?: (replacement: AnnotatedStringReplacement<T>) => boolean): StringEdit {
550 const newReplacements: StringReplacement[] = [];
551 for (const r of this.replacements) {
556 return new StringEdit(newReplacements);
557 }
558 > } stringEdit.ts
559 >
560 > export class AnnotatedStringReplacement<T extends IEditData<T>> extends BaseStringReplacement<AnnotatedStringReplacement<T>> {
561 > public static insert<T extends IEditData<T>>(offset: number, text: string, data: T): AnnotatedStringReplacement<T> {
562 > return new AnnotatedStringReplacement<T>(OffsetRange.emptyAt(offset), text, data);
563 > }
564 >
565 > public static replace<T extends IEditData<T>>(range: OffsetRange, text: string, data: T): AnnotatedStringReplacement<T> {
566 return new AnnotatedStringReplacement<T>(range, text, data);
567 }
569 > public static delete<T extends IEditData<T>>(range: OffsetRange, data: T): AnnotatedStringReplacement<T> {
570 return new AnnotatedStringReplacement<T>(range, '', data);
571 }
573 > constructor(
574 range: OffsetRange,
575 newText: string,
578 super(range, newText);
579 }
581 > override equals(other: AnnotatedStringReplacement<T>): boolean {
582 return this.replaceRange.equals(other.replaceRange) && this.newText === other.newText && this.data === other.data;
583 }
585 > tryJoinTouching(other: AnnotatedStringReplacement<T>): AnnotatedStringReplacement<T> | undefined {
586 const joined = this.data.join(other.data);
587 if (joined === undefined) {
590 return new AnnotatedStringReplacement(this.replaceRange.joinRightTouching(other.replaceRange), this.newText + other.newText, joined);
591 }
593 > slice(range: OffsetRange, rangeInReplacement?: OffsetRange): AnnotatedStringReplacement<T> {
594 return new AnnotatedStringReplacement(range, rangeInReplacement ? rangeInReplacement.substring(this.newText) : this.newText, this.data);
595 }
596 > } stringEdit.ts
597 >
598 > /**
599 > * Returns true if both ranges are empty (inserts) at the exact same position.
600 > * In this case, although they don't "intersect" in the traditional sense,
601 > * they conflict because the order of insertion matters.
602 > */
603 function areConcurrentInserts(r1: OffsetRange, r2: OffsetRange): boolean {
604 return r1.isEmpty && r2.isEmpty && r1.start === r2.start;
605 }
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;
src/vs/editor/common/core/edits/edit.ts 144 introduced LOC · 30 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- edit.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { sumBy } from '../../../../base/common/arrays.js';
7 > import { BugIndicatingError } from '../../../../base/common/errors.js';
8 > import { OffsetRange } from '../ranges/offsetRange.js';
9 >
10 > // eslint-disable-next-line @typescript-eslint/no-explicit-any
11 > export abstract class BaseEdit<T extends BaseReplacement<T> = BaseReplacement<any>, TEdit extends BaseEdit<T, TEdit> = BaseEdit<T, any>> {
12 > constructor(
13 > public readonly replacements: readonly T[],
14 > ) {
15 > let lastEndEx = -1;
16 > for (const replacement of replacements) {
17 if (!(replacement.replaceRange.start >= lastEndEx)) {
18 throw new BugIndicatingError(`Edits must be disjoint and sorted. Found ${replacement} after ${lastEndEx}`);
20 lastEndEx = replacement.replaceRange.endExclusive;
21 }
22 > } edit.ts
23 >
24 > protected abstract _createNew(replacements: readonly T[]): TEdit;
25 >
26 > /**
27 > * Returns true if and only if this edit and the given edit are structurally equal.
28 > * Note that this does not mean that the edits have the same effect on a given input!
29 > * See `.normalize()` or `.normalizeOnBase(base)` for that.
30 > */
31 > public equals(other: TEdit): boolean {
32 if (this.replacements.length !== other.replacements.length) {
33 return false;
40 return true;
41 }
42 > edit.ts
43 > public toString() {
44 const edits = this.replacements.map(e => e.toString()).join(', ');
45 return `[${edits}]`;
46 }
47 > edit.ts
48 > /**
49 > * Normalizes the edit by removing empty replacements and joining touching replacements (if the replacements allow joining).
50 > * Two edits have an equal normalized edit if and only if they have the same effect on any input.
51 > *
52 > * ![](https://raw.githubusercontent.com/microsoft/vscode/refs/heads/main/src/vs/editor/common/core/edits/docs/BaseEdit_normalize.drawio.png)
53 > *
54 > * Invariant:
55 > * ```
56 > * (forall base: TEdit.apply(base).equals(other.apply(base))) <-> this.normalize().equals(other.normalize())
57 > * ```
58 > * and
59 > * ```
60 > * forall base: TEdit.apply(base).equals(this.normalize().apply(base))
61 > * ```
62 > *
63 > */
64 > public normalize(): TEdit {
65 const newReplacements: T[] = [];
66 let lastReplacement: T | undefined;
88 return this._createNew(newReplacements);
89 }
90 > edit.ts
91 > /**
92 > * Combines two edits into one with the same effect.
93 > *
94 > * ![](https://raw.githubusercontent.com/microsoft/vscode/refs/heads/main/src/vs/editor/common/core/edits/docs/BaseEdit_compose.drawio.png)
95 > *
96 > * Invariant:
97 > * ```
98 > * other.apply(this.apply(s0)) = this.compose(other).apply(s0)
99 > * ```
100 > */
101 > public compose(other: TEdit): TEdit {
102 const edits1 = this.normalize();
103 const edits2 = other.normalize();
184 return this._createNew(result).normalize();
185 }
186 > edit.ts
187 > public decomposeSplit(shouldBeInE1: (repl: T) => boolean): { e1: TEdit; e2: TEdit } {
188 const e1: T[] = [];
189 const e2: T[] = [];
200 return { e1: this._createNew(e1), e2: this._createNew(e2) };
201 }
202 > edit.ts
203 > /**
204 > * Returns the range of each replacement in the applied value.
205 > */
206 > public getNewRanges(): OffsetRange[] {
207 const ranges: OffsetRange[] = [];
208 let offset = 0;
213 return ranges;
214 }
215 > edit.ts
216 > public getJoinedReplaceRange(): OffsetRange | undefined {
217 if (this.replacements.length === 0) {
218 return undefined;
220 return this.replacements[0].replaceRange.join(this.replacements.at(-1)!.replaceRange);
221 }
222 > edit.ts
223 > public isEmpty(): boolean {
224 return this.replacements.length === 0;
225 }
226 > edit.ts
227 > public getLengthDelta(): number {
228 return sumBy(this.replacements, (replacement) => replacement.getLengthDelta());
229 }
230 > edit.ts
231 > public getNewDataLength(dataLength: number): number {
232 return dataLength + this.getLengthDelta();
233 }
234 > edit.ts
235 > public applyToOffset(originalOffset: number): number {
236 let accumulatedDelta = 0;
237 for (const r of this.replacements) {
248 return originalOffset + accumulatedDelta;
249 }
250 > edit.ts
251 > public applyToOffsetRange(originalRange: OffsetRange): OffsetRange {
252 return new OffsetRange(
253 this.applyToOffset(originalRange.start),
255 );
256 }
257 > edit.ts
258 > public applyInverseToOffset(postEditsOffset: number): number {
259 let accumulatedDelta = 0;
260 for (const edit of this.replacements) {
272 return postEditsOffset - accumulatedDelta;
273 }
274 > edit.ts
275 > /**
276 > * Return undefined if the originalOffset is within an edit
277 > */
278 > public applyToOffsetOrUndefined(originalOffset: number): number | undefined {
279 let accumulatedDelta = 0;
280 for (const edit of this.replacements) {
291 return originalOffset + accumulatedDelta;
292 }
293 > edit.ts
294 > /**
295 > * Return undefined if the originalRange is within an edit
296 > */
297 > public applyToOffsetRangeOrUndefined(originalRange: OffsetRange): OffsetRange | undefined {
298 const start = this.applyToOffsetOrUndefined(originalRange.start);
299 if (start === undefined) {
306 return new OffsetRange(start, end);
307 }
308 > } edit.ts
309 >
310 > export abstract class BaseReplacement<TSelf extends BaseReplacement<TSelf>> {
311 > constructor(
312 /**
313 * The range to be replaced.
315 public readonly replaceRange: OffsetRange,
316 ) { }
317 > edit.ts
318 > public abstract getNewLength(): number;
319 >
320 > /**
321 > * Precondition: TEdit.range.endExclusive === other.range.start
322 > */
323 > public abstract tryJoinTouching(other: TSelf): TSelf | undefined;
324 >
325 > public abstract slice(newReplaceRange: OffsetRange, rangeInReplacement?: OffsetRange): TSelf;
326 >
327 > public delta(offset: number): TSelf {
328 return this.slice(this.replaceRange.delta(offset), new OffsetRange(0, this.getNewLength()));
329 }
330 > edit.ts
331 > public getLengthDelta(): number {
332 return this.getNewLength() - this.replaceRange.length;
333 }
334 > edit.ts
335 > abstract equals(other: TSelf): boolean;
336 >
337 > toString(): string {
338 return `{ ${this.replaceRange.toString()} -> ${this.getNewLength()} }`;
339 }
340 > edit.ts
341 > get isEmpty() {
342 return this.getNewLength() === 0 && this.replaceRange.length === 0;
343 }
344 > edit.ts
345 > getRangeAfterReplace(): OffsetRange {
346 return new OffsetRange(this.replaceRange.start, this.replaceRange.start + this.getNewLength());
347 }
348 > } edit.ts
349 >
350 > export type AnyEdit = BaseEdit<AnyReplacement, AnyEdit>;
351 > export type AnyReplacement = BaseReplacement<AnyReplacement>;
352 >
353 > export class Edit<T extends BaseReplacement<T>> extends BaseEdit<T, Edit<T>> {
354 > /**
355 > * Represents a set of edits to a string.
356 > * All these edits are applied at once.
357 > */
358 > public static readonly empty = new Edit<never>([]);
359 >
360 > public static create<T extends BaseReplacement<T>>(replacements: readonly T[]): Edit<T> {
361 return new Edit(replacements);
362 }
363 > edit.ts
364 > public static single<T extends BaseReplacement<T>>(replacement: T): Edit<T> {
365 return new Edit([replacement]);
366 }
367 > edit.ts
368 > protected override _createNew(replacements: readonly T[]): Edit<T> {
369 return new Edit(replacements);
370 }
371 > } edit.ts
372 >
373 > export class AnnotationReplacement<TAnnotation> extends BaseReplacement<AnnotationReplacement<TAnnotation>> {
374 > constructor(
375 range: OffsetRange,
376 public readonly newLength: number,
379 super(range);
380 }
381 > edit.ts
382 > override equals(other: AnnotationReplacement<TAnnotation>): boolean {
383 return this.replaceRange.equals(other.replaceRange) && this.newLength === other.newLength && this.annotation === other.annotation;
384 }
385 > edit.ts
386 > getNewLength(): number { return this.newLength; }
387 >
388 > tryJoinTouching(other: AnnotationReplacement<TAnnotation>): AnnotationReplacement<TAnnotation> | undefined {
389 if (this.annotation !== other.annotation) {
390 return undefined;
392 return new AnnotationReplacement<TAnnotation>(this.replaceRange.joinRightTouching(other.replaceRange), this.newLength + other.newLength, this.annotation);
393 }
394 > edit.ts
395 > slice(range: OffsetRange, rangeInReplacement?: OffsetRange): AnnotationReplacement<TAnnotation> {
396 return new AnnotationReplacement<TAnnotation>(range, rangeInReplacement ? rangeInReplacement.length : this.newLength, this.annotation);
397 }
398 > } edit.ts