diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 48538173e67..451f8ca1b85 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -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 (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 (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 (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); + } } \ No newline at end of file diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index deefb2a4052..c8186b6af7a 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -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)); } } diff --git a/src/services/services.ts b/src/services/services.ts index 569754ffaba..868850085d8 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -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); diff --git a/src/services/shims.ts b/src/services/shims.ts index c07ef320fdb..459a56518bd 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -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); } } diff --git a/src/services/text.ts b/src/services/text.ts index 05b460cda08..12cb1b9d4b7 100644 --- a/src/services/text.ts +++ b/src/services/text.ts @@ -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); - } - } } \ No newline at end of file diff --git a/tests/cases/unittests/incrementalParser.ts b/tests/cases/unittests/incrementalParser.ts index 4cd816a3a3a..9e7e307b069 100644 --- a/tests/cases/unittests/incrementalParser.ts +++ b/tests/cases/unittests/incrementalParser.ts @@ -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; } {