Move textSpan and textChangeRange impls to the compiler layer.

This commit is contained in:
Cyrus Najmabadi 2014-12-10 14:36:37 -08:00
parent c2d4cd5887
commit 888b88ee43
6 changed files with 232 additions and 316 deletions

View File

@ -1620,13 +1620,6 @@ module ts {
getNewLine(): string;
}
export interface TextChangeRange {
span(): TextSpan;
newLength(): number;
newSpan(): TextSpan;
isUnchanged(): boolean;
}
export interface TextSpan {
start(): number;
length(): number;
@ -1642,160 +1635,245 @@ module ts {
intersection(span: TextSpan): TextSpan;
}
/**
* Creates a TextSpan instance beginning with the position Start and having the Length
* specified with length.
*/
/*
var textSpanConstructor = (function () {
function textSpanConstructor(start: number, length: number) {
if (start < 0) {
throw new Error("start < 0");
}
if (length < 0) {
throw new Error("start < 0");
}
this._start = start;
this._length = length;
}
textSpanConstructor.prototype = {
toJSON(key: string) {
return { start: this._start, length: this._length }
},
start() {
return this._start
},
length() {
return this._length
},
end() {
return this._start + this._length
},
isEmpty() {
return this._length === 0
},
containsPosition(position: number) {
return position >= this._start && position < this.end()
},
containsTextSpan(span: TextSpan) {
return span.start() >= this._start && span.end() <= this.end()
},
overlapsWith(span: TextSpan) {
var overlapStart = Math.max(this._start, span.start());
var overlapEnd = Math.min(this.end(), span.end());
return overlapStart < overlapEnd;
},
overlap(span: TextSpan) {
var overlapStart = Math.max(this._start, span.start());
var overlapEnd = Math.min(this.end(), span.end());
if (overlapStart < overlapEnd) {
return createTextSpanFromBounds(overlapStart, overlapEnd);
}
return undefined;
},
intersectsWithTextSpan(span: TextSpan) {
return span.start() <= this.end() && span.end() >= this._start
},
intersectsWith(start: number, length: number) {
var end = start + length;
return start <= this.end() && end >= this._start;
},
intersectsWithPosition(position: number) {
return position <= this.end() && position >= this._start;
},
intersection(span: TextSpan) {
var intersectStart = Math.max(this._start, span.start());
var intersectEnd = Math.min(this.end(), span.end());
if (intersectStart <= intersectEnd) {
return createTextSpanFromBounds(intersectStart, intersectEnd);
}
return undefined;
}
};
return textSpanConstructor;
})();
export function createTextSpan(start: number, length: number): TextSpan {
Debug.assert(start >= 0, "start");
Debug.assert(length >= 0, "length");
function end() {
return start + length;
}
function overlapsWith(span: TextSpan): boolean {
var overlapStart = Math.max(start, span.start());
var overlapEnd = Math.min(end(), span.end());
return overlapStart < overlapEnd;
}
function overlap(span: TextSpan): TextSpan {
var overlapStart = Math.max(start, span.start());
var overlapEnd = Math.min(end(), span.end());
if (overlapStart < overlapEnd) {
return createTextSpanFromBounds(overlapStart, overlapEnd);
}
return undefined;
}
function intersectsWithTextSpan(span: TextSpan): boolean {
return span.start() <= end() && span.end() >= start;
}
function intersectsWith(_start: number, _length: number): boolean {
var _end = _start + _length;
return _start <= end() && _end >= start;
}
function intersectsWithPosition(position: number): boolean {
return position <= end() && position >= start;
}
function intersection(span: TextSpan): TextSpan {
var intersectStart = Math.max(start, span.start());
var intersectEnd = Math.min(end(), span.end());
if (intersectStart <= intersectEnd) {
return createTextSpanFromBounds(intersectStart, intersectEnd);
}
return undefined;
}
return {
start: () => start, length: () => length,
toJSON: key => { start, length },
end: end,
isEmpty: () => length === 0,
containsPosition: position => position >= start && position < end(),
containsTextSpan: span => span.start() >= start && span.end() <= end(),
overlapsWith: overlapsWith,
overlap: overlap,
intersectsWithTextSpan: intersectsWithTextSpan,
intersectsWith: intersectsWith,
intersectsWithPosition: intersectsWithPosition,
intersection: intersection,
}
return new (<any>textSpanConstructor)(start, length);
}
*/
export function createTextSpanFromBounds(start: number, end: number) {
return createTextSpan(start, end - start);
}
export function createTextSpan(start: number, length: number): TextSpan {
return new (<any>textSpanConstructor)(start, length);
export interface TextChangeRange {
span(): TextSpan;
newLength(): number;
newSpan(): TextSpan;
isUnchanged(): boolean;
}
var textSpanConstructor = (function () {
function TextSpanObject(start: number, length: number) {
ts.Debug.assert(start >= 0, "start");
ts.Debug.assert(length >= 0, "length");
this._start = start;
this._length = length;
var textChangeRangeConstructor = (function () {
function textChangeRangeConstructor(span: TextSpan, newLength: number) {
if (newLength < 0) {
throw new Error("newLength < 0");
}
this._span = span;
this._newLength = newLength;
}
TextSpanObject.prototype.toJSON = function (key: string) {
return { start: this._start, length: this._length };
};
TextSpanObject.prototype.start = function () {
return this._start;
};
TextSpanObject.prototype.length = function () {
return this._length;
};
TextSpanObject.prototype.end = function () {
return this._start + this._length;
};
TextSpanObject.prototype.isEmpty = function () {
return this._length === 0;
};
TextSpanObject.prototype.containsPosition = function (position: number) {
return position >= this._start && position < this.end();
};
TextSpanObject.prototype.containsTextSpan = function (span: TextSpan) {
return span.start() >= this._start && span.end() <= this.end();
};
TextSpanObject.prototype.overlapsWith = function (span: TextSpan) {
var overlapStart = Math.max(this._start, span.start());
var overlapEnd = Math.min(this.end(), span.end());
return overlapStart < overlapEnd;
};
TextSpanObject.prototype.overlap = function (span: TextSpan) {
var overlapStart = Math.max(this._start, span.start());
var overlapEnd = Math.min(this.end(), span.end());
if (overlapStart < overlapEnd) {
return createTextSpanFromBounds(overlapStart, overlapEnd);
textChangeRangeConstructor.prototype = {
span() {
return this._span;
},
newLength() {
return this._newLength;
},
newSpan() {
return createTextSpan(this.span().start(), this.newLength());
},
isUnchanged() {
return this.span().isEmpty() && this.newLength() === 0;
}
return undefined;
};
TextSpanObject.prototype.intersectsWithTextSpan = function (span: TextSpan) {
return span.start() <= this.end() && span.end() >= this._start;
};
TextSpanObject.prototype.intersectsWith = function (start: number, length: number) {
var end = start + length;
return start <= this.end() && end >= this._start;
};
TextSpanObject.prototype.intersectsWithPosition = function (position: number) {
return position <= this.end() && position >= this._start;
};
TextSpanObject.prototype.intersection = function (span: TextSpan) {
var intersectStart = Math.max(this._start, span.start());
var intersectEnd = Math.min(this.end(), span.end());
if (intersectStart <= intersectEnd) {
return createTextSpanFromBounds(intersectStart, intersectEnd);
}
return undefined;
};
return TextSpanObject;
return textChangeRangeConstructor;
})();
export function createTextChangeRange(span: TextSpan, newLength: number): TextChangeRange {
return new (<any>textChangeRangeConstructor)(span, newLength);
}
export var unchangedTextChangeRange = createTextChangeRange(createTextSpan(0, 0), 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.
*/
export function collapseTextChangeRangesAcrossMultipleVersions(changes: TextChangeRange[]): TextChangeRange {
if (changes.length === 0) {
return unchangedTextChangeRange;
}
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 createTextChangeRange(createTextSpanFromBounds(oldStartN, oldEndN), /*newLength: */newEndN - oldStartN);
}
}

View File

@ -32,7 +32,7 @@ module Harness.LanguageService {
// Store edit range + new length of script
this.editRanges.push({
length: this.content.length,
textChangeRange: new ts.TextChangeRangeObject(
textChangeRange: ts.createTextChangeRange(
ts.createTextSpanFromBounds(minChar, limChar), newText.length)
});
@ -43,14 +43,14 @@ module Harness.LanguageService {
public getTextChangeRangeBetweenVersions(startVersion: number, endVersion: number): ts.TextChangeRange {
if (startVersion === endVersion) {
// No edits!
return ts.TextChangeRangeObject.unchanged;
return ts.unchangedTextChangeRange;
}
var initialEditRangeIndex = this.editRanges.length - (this.version - startVersion);
var lastEditRangeIndex = this.editRanges.length - (this.version - endVersion);
var entries = this.editRanges.slice(initialEditRangeIndex, lastEditRangeIndex);
return ts.TextChangeRangeObject.collapseChangesAcrossMultipleVersions(entries.map(e => e.textChangeRange));
return ts.collapseTextChangeRangesAcrossMultipleVersions(entries.map(e => e.textChangeRange));
}
}

View File

@ -1613,7 +1613,7 @@ module ts {
public getChangeRange(filename: string, lastKnownVersion: string, oldScriptSnapshot: IScriptSnapshot): TextChangeRange {
var currentVersion = this.getVersion(filename);
if (lastKnownVersion === currentVersion) {
return TextChangeRangeObject.unchanged; // "No changes"
return unchangedTextChangeRange; // "No changes"
}
var scriptSnapshot = this.getScriptSnapshot(filename);

View File

@ -328,7 +328,7 @@ module ts {
}
var decoded: { span: { start: number; length: number; }; newLength: number; } = JSON.parse(encoded);
return new TextChangeRangeObject(
return createTextChangeRange(
createTextSpan(decoded.span.start, decoded.span.length), decoded.newLength);
}
}

View File

@ -1,164 +1,2 @@
module ts {
export class TextChangeRangeObject implements TextChangeRange {
public static unchanged = new TextChangeRangeObject(createTextSpan(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 createTextSpan(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 TextChangeRangeObject.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 TextChangeRangeObject(createTextSpanFromBounds(oldStartN, oldEndN), /*newLength: */newEndN - oldStartN);
}
}
}

View File

@ -6,7 +6,7 @@ module ts {
var contents = text.getText(0, text.getLength());
var newContents = contents.substr(0, start) + newText + contents.substring(start + length);
return { text: ScriptSnapshot.fromString(newContents), textChangeRange: new TextChangeRangeObject(createTextSpan(start, length), newText.length) }
return { text: ScriptSnapshot.fromString(newContents), textChangeRange: createTextChangeRange(createTextSpan(start, length), newText.length) }
}
function withInsert(text: IScriptSnapshot, start: number, newText: string): { text: IScriptSnapshot; textChangeRange: TextChangeRange; } {