mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-19 01:33:15 -05:00
296 lines
16 KiB
TypeScript
296 lines
16 KiB
TypeScript
module ts {
|
|
export class TextSpan {
|
|
private _start: number;
|
|
private _length: number;
|
|
|
|
/**
|
|
* Creates a TextSpan instance beginning with the position Start and having the Length
|
|
* specified with length.
|
|
*/
|
|
constructor(start: number, length: number) {
|
|
Debug.assert(start >= 0, "start");
|
|
Debug.assert(length >= 0, "length");
|
|
|
|
this._start = start;
|
|
this._length = length;
|
|
}
|
|
|
|
public toJSON(key: any): any {
|
|
return { start: this._start, length: this._length };
|
|
}
|
|
|
|
public start(): number {
|
|
return this._start;
|
|
}
|
|
|
|
public length(): number {
|
|
return this._length;
|
|
}
|
|
|
|
public end(): number {
|
|
return this._start + this._length;
|
|
}
|
|
|
|
public isEmpty(): boolean {
|
|
return this._length === 0;
|
|
}
|
|
|
|
/**
|
|
* Determines whether the position lies within the span. Returns true if the position is greater than or equal to Start and strictly less
|
|
* than End, otherwise false.
|
|
* @param position The position to check.
|
|
*/
|
|
public containsPosition(position: number): boolean {
|
|
return position >= this._start && position < this.end();
|
|
}
|
|
|
|
/**
|
|
* Determines whether span falls completely within this span. Returns true if the specified span falls completely within this span, otherwise false.
|
|
* @param span The span to check.
|
|
*/
|
|
public containsTextSpan(span: TextSpan): boolean {
|
|
return span._start >= this._start && span.end() <= this.end();
|
|
}
|
|
|
|
/**
|
|
* Determines whether the given span overlaps this span. Two spans are considered to overlap
|
|
* if they have positions in common and neither is empty. Empty spans do not overlap with any
|
|
* other span. Returns true if the spans overlap, false otherwise.
|
|
* @param span The span to check.
|
|
*/
|
|
public overlapsWith(span: TextSpan): boolean {
|
|
var overlapStart = Math.max(this._start, span._start);
|
|
var overlapEnd = Math.min(this.end(), span.end());
|
|
|
|
return overlapStart < overlapEnd;
|
|
}
|
|
|
|
/**
|
|
* Returns the overlap with the given span, or undefined if there is no overlap.
|
|
* @param span The span to check.
|
|
*/
|
|
public overlap(span: TextSpan): TextSpan {
|
|
var overlapStart = Math.max(this._start, span._start);
|
|
var overlapEnd = Math.min(this.end(), span.end());
|
|
|
|
if (overlapStart < overlapEnd) {
|
|
return TextSpan.fromBounds(overlapStart, overlapEnd);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Determines whether span intersects this span. Two spans are considered to
|
|
* intersect if they have positions in common or the end of one span
|
|
* coincides with the start of the other span. Returns true if the spans intersect, false otherwise.
|
|
* @param The span to check.
|
|
*/
|
|
public intersectsWithTextSpan(span: TextSpan): boolean {
|
|
return span._start <= this.end() && span.end() >= this._start;
|
|
}
|
|
|
|
public intersectsWith(start: number, length: number): boolean {
|
|
var end = start + length;
|
|
return start <= this.end() && end >= this._start;
|
|
}
|
|
|
|
/**
|
|
* Determines whether the given position intersects this span.
|
|
* A position is considered to intersect if it is between the start and
|
|
* end positions (inclusive) of this span. Returns true if the position intersects, false otherwise.
|
|
* @param position The position to check.
|
|
*/
|
|
public intersectsWithPosition(position: number): boolean {
|
|
return position <= this.end() && position >= this._start;
|
|
}
|
|
|
|
/**
|
|
* Returns the intersection with the given span, or undefined if there is no intersection.
|
|
* @param span The span to check.
|
|
*/
|
|
public intersection(span: TextSpan): TextSpan {
|
|
var intersectStart = Math.max(this._start, span._start);
|
|
var intersectEnd = Math.min(this.end(), span.end());
|
|
|
|
if (intersectStart <= intersectEnd) {
|
|
return TextSpan.fromBounds(intersectStart, intersectEnd);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Creates a new TextSpan from the given start and end positions
|
|
* as opposed to a position and length.
|
|
*/
|
|
public static fromBounds(start: number, end: number): TextSpan {
|
|
Debug.assert(start >= 0);
|
|
Debug.assert(end - start >= 0);
|
|
return new TextSpan(start, end - start);
|
|
}
|
|
}
|
|
|
|
export class TextChangeRange {
|
|
public static unchanged = new TextChangeRange(new TextSpan(0, 0), 0);
|
|
|
|
private _span: TextSpan;
|
|
private _newLength: number;
|
|
|
|
/**
|
|
* Initializes a new instance of TextChangeRange.
|
|
*/
|
|
constructor(span: TextSpan, newLength: number) {
|
|
Debug.assert(newLength >= 0, "newLength");
|
|
|
|
this._span = span;
|
|
this._newLength = newLength;
|
|
}
|
|
|
|
/**
|
|
* The span of text before the edit which is being changed
|
|
*/
|
|
public span(): TextSpan {
|
|
return this._span;
|
|
}
|
|
|
|
/**
|
|
* Width of the span after the edit. A 0 here would represent a delete
|
|
*/
|
|
public newLength(): number {
|
|
return this._newLength;
|
|
}
|
|
|
|
public newSpan(): TextSpan {
|
|
return new TextSpan(this.span().start(), this.newLength());
|
|
}
|
|
|
|
public isUnchanged(): boolean {
|
|
return this.span().isEmpty() && this.newLength() === 0;
|
|
}
|
|
|
|
/**
|
|
* Called to merge all the changes that occurred across several versions of a script snapshot
|
|
* into a single change. i.e. if a user keeps making successive edits to a script we will
|
|
* have a text change from V1 to V2, V2 to V3, ..., Vn.
|
|
*
|
|
* This function will then merge those changes into a single change range valid between V1 and
|
|
* Vn.
|
|
*/
|
|
public static collapseChangesAcrossMultipleVersions(changes: TextChangeRange[]): TextChangeRange {
|
|
if (changes.length === 0) {
|
|
return TextChangeRange.unchanged;
|
|
}
|
|
|
|
if (changes.length === 1) {
|
|
return changes[0];
|
|
}
|
|
|
|
// We change from talking about { { oldStart, oldLength }, newLength } to { oldStart, oldEnd, newEnd }
|
|
// as it makes things much easier to reason about.
|
|
var change0 = changes[0];
|
|
|
|
var oldStartN = change0.span().start();
|
|
var oldEndN = change0.span().end();
|
|
var newEndN = oldStartN + change0.newLength();
|
|
|
|
for (var i = 1; i < changes.length; i++) {
|
|
var nextChange = changes[i];
|
|
|
|
// Consider the following case:
|
|
// i.e. two edits. The first represents the text change range { { 10, 50 }, 30 }. i.e. The span starting
|
|
// at 10, with length 50 is reduced to length 30. The second represents the text change range { { 30, 30 }, 40 }.
|
|
// i.e. the span starting at 30 with length 30 is increased to length 40.
|
|
//
|
|
// 0 10 20 30 40 50 60 70 80 90 100
|
|
// -------------------------------------------------------------------------------------------------------
|
|
// | /
|
|
// | /----
|
|
// T1 | /----
|
|
// | /----
|
|
// | /----
|
|
// -------------------------------------------------------------------------------------------------------
|
|
// | \
|
|
// | \
|
|
// T2 | \
|
|
// | \
|
|
// | \
|
|
// -------------------------------------------------------------------------------------------------------
|
|
//
|
|
// Merging these turns out to not be too difficult. First, determining the new start of the change is trivial
|
|
// it's just the min of the old and new starts. i.e.:
|
|
//
|
|
// 0 10 20 30 40 50 60 70 80 90 100
|
|
// ------------------------------------------------------------*------------------------------------------
|
|
// | /
|
|
// | /----
|
|
// T1 | /----
|
|
// | /----
|
|
// | /----
|
|
// ----------------------------------------$-------------------$------------------------------------------
|
|
// . | \
|
|
// . | \
|
|
// T2 . | \
|
|
// . | \
|
|
// . | \
|
|
// ----------------------------------------------------------------------*--------------------------------
|
|
//
|
|
// (Note the dots represent the newly inferrred start.
|
|
// Determining the new and old end is also pretty simple. Basically it boils down to paying attention to the
|
|
// absolute positions at the asterixes, and the relative change between the dollar signs. Basically, we see
|
|
// which if the two $'s precedes the other, and we move that one forward until they line up. in this case that
|
|
// means:
|
|
//
|
|
// 0 10 20 30 40 50 60 70 80 90 100
|
|
// --------------------------------------------------------------------------------*----------------------
|
|
// | /
|
|
// | /----
|
|
// T1 | /----
|
|
// | /----
|
|
// | /----
|
|
// ------------------------------------------------------------$------------------------------------------
|
|
// . | \
|
|
// . | \
|
|
// T2 . | \
|
|
// . | \
|
|
// . | \
|
|
// ----------------------------------------------------------------------*--------------------------------
|
|
//
|
|
// In other words (in this case), we're recognizing that the second edit happened after where the first edit
|
|
// ended with a delta of 20 characters (60 - 40). Thus, if we go back in time to where the first edit started
|
|
// that's the same as if we started at char 80 instead of 60.
|
|
//
|
|
// As it so happens, the same logic applies if the second edit precedes the first edit. In that case rahter
|
|
// than pusing the first edit forward to match the second, we'll push the second edit forward to match the
|
|
// first.
|
|
//
|
|
// In this case that means we have { oldStart: 10, oldEnd: 80, newEnd: 70 } or, in TextChangeRange
|
|
// semantics: { { start: 10, length: 70 }, newLength: 60 }
|
|
//
|
|
// The math then works out as follows.
|
|
// If we have { oldStart1, oldEnd1, newEnd1 } and { oldStart2, oldEnd2, newEnd2 } then we can compute the
|
|
// final result like so:
|
|
//
|
|
// {
|
|
// oldStart3: Min(oldStart1, oldStart2),
|
|
// oldEnd3 : Max(oldEnd1, oldEnd1 + (oldEnd2 - newEnd1)),
|
|
// newEnd3 : Max(newEnd2, newEnd2 + (newEnd1 - oldEnd2))
|
|
// }
|
|
|
|
var oldStart1 = oldStartN;
|
|
var oldEnd1 = oldEndN;
|
|
var newEnd1 = newEndN;
|
|
|
|
var oldStart2 = nextChange.span().start();
|
|
var oldEnd2 = nextChange.span().end();
|
|
var newEnd2 = oldStart2 + nextChange.newLength();
|
|
|
|
oldStartN = Math.min(oldStart1, oldStart2);
|
|
oldEndN = Math.max(oldEnd1, oldEnd1 + (oldEnd2 - newEnd1));
|
|
newEndN = Math.max(newEnd2, newEnd2 + (newEnd1 - oldEnd2));
|
|
}
|
|
|
|
return new TextChangeRange(TextSpan.fromBounds(oldStartN, oldEndN), /*newLength: */newEndN - oldStartN);
|
|
}
|
|
}
|
|
} |