diff --git a/src/vs/editor/common/core/lineRange.ts b/src/vs/editor/common/core/lineRange.ts index 568bf03ca2b..73b0f7e5761 100644 --- a/src/vs/editor/common/core/lineRange.ts +++ b/src/vs/editor/common/core/lineRange.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { BugIndicatingError } from 'vs/base/common/errors'; +import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { Range } from 'vs/editor/common/core/range'; /** @@ -239,6 +240,13 @@ export class LineRange { public includes(lineNumber: number): boolean { return this.startLineNumber <= lineNumber && lineNumber < this.endLineNumberExclusive; } + + /** + * Converts this 1-based line range to a 0-based offset range (subtracts 1!). + */ + public toOffsetRange(): OffsetRange { + return new OffsetRange(this.startLineNumber - 1, this.endLineNumberExclusive - 1); + } } export type ISerializedLineRange = [startLineNumber: number, endLineNumberExclusive: number]; diff --git a/src/vs/editor/common/core/offsetRange.ts b/src/vs/editor/common/core/offsetRange.ts index d46cf88a425..d4129b1c532 100644 --- a/src/vs/editor/common/core/offsetRange.ts +++ b/src/vs/editor/common/core/offsetRange.ts @@ -48,6 +48,14 @@ export class OffsetRange { return new OffsetRange(this.start + offset, this.endExclusive + offset); } + public deltaStart(offset: number): OffsetRange { + return new OffsetRange(this.start + offset, this.endExclusive); + } + + public deltaEnd(offset: number): OffsetRange { + return new OffsetRange(this.start, this.endExclusive + offset); + } + public get length(): number { return this.endExclusive - this.start; } @@ -90,6 +98,10 @@ export class OffsetRange { } return undefined; } + + public slice(arr: T[]): T[] { + return arr.slice(this.start, this.endExclusive); + } } export class OffsetRangeSet { diff --git a/src/vs/editor/common/diff/linesDiffComputer.ts b/src/vs/editor/common/diff/linesDiffComputer.ts index 84505ab563a..d10888cb93f 100644 --- a/src/vs/editor/common/diff/linesDiffComputer.ts +++ b/src/vs/editor/common/diff/linesDiffComputer.ts @@ -155,6 +155,13 @@ export class SimpleLineRangeMapping { public flip(): SimpleLineRangeMapping { return new SimpleLineRangeMapping(this.modified, this.original); } + + public join(other: SimpleLineRangeMapping): SimpleLineRangeMapping { + return new SimpleLineRangeMapping( + this.original.join(other.original), + this.modified.join(other.modified), + ); + } } export class MovedText { diff --git a/src/vs/editor/common/diff/standardLinesDiffComputer.ts b/src/vs/editor/common/diff/standardLinesDiffComputer.ts index b9ef58b85c2..20279a9fe68 100644 --- a/src/vs/editor/common/diff/standardLinesDiffComputer.ts +++ b/src/vs/editor/common/diff/standardLinesDiffComputer.ts @@ -177,7 +177,7 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer { } private computeMoves(changes: LineRangeMapping[], originalLines: string[], modifiedLines: string[], hashedOriginalLines: number[], hashedModifiedLines: number[], timeout: ITimeout, considerWhitespaceChanges: boolean): MovedText[] { - const moves: MovedText[] = []; + const moves: SimpleLineRangeMapping[] = []; const deletions = changes .filter(c => c.modifiedRange.isEmpty && c.originalRange.length >= 3) .map(d => new LineRangeFragment(d.originalRange, originalLines, d)); @@ -199,17 +199,15 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer { } if (highestSimilarity > 0.90 && best) { - const moveChanges = this.refineDiff(originalLines, modifiedLines, new SequenceDiff( - new OffsetRange(deletion.range.startLineNumber - 1, deletion.range.endLineNumberExclusive - 1), - new OffsetRange(best.range.startLineNumber - 1, best.range.endLineNumberExclusive - 1) - ), timeout, considerWhitespaceChanges); - const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, originalLines, modifiedLines, true); - insertions.delete(best); - moves.push(new MovedText(new SimpleLineRangeMapping(deletion.range, best.range), mappings)); + moves.push(new SimpleLineRangeMapping(deletion.range, best.range)); excludedChanges.add(deletion.source); excludedChanges.add(best.source); } + + if (!timeout.isValid()) { + return []; + } } const original3LineHashes = new SetMap(); @@ -242,9 +240,6 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer { let lastMappings: PossibleMapping[] = []; for (let i = change.modifiedRange.startLineNumber; i < change.modifiedRange.endLineNumberExclusive - 2; i++) { const key = `${hashedModifiedLines[i - 1]}:${hashedModifiedLines[i + 1 - 1]}:${hashedModifiedLines[i + 2 - 1]}`; - - //const isWeakKey = (originalLines[i].trim().length + originalLines[i + 1].trim().length + originalLines[i + 2].trim().length < 20); - const currentModifiedRange = new LineRange(i, i + 3); const nextMappings: PossibleMapping[] = []; @@ -269,6 +264,10 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer { }); lastMappings = nextMappings; } + + if (!timeout.isValid()) { + return []; + } } possibleMappings.sort(reverseOrder(compareBy(m => m.modifiedLineRange.length, numberComparator))); @@ -291,19 +290,49 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer { const modifiedLineRange = s; const originalLineRange = s.delta(-diffOrigToMod); - const moveChanges = this.refineDiff(originalLines, modifiedLines, new SequenceDiff( - new OffsetRange(originalLineRange.startLineNumber - 1, originalLineRange.endLineNumberExclusive - 1), - new OffsetRange(modifiedLineRange.startLineNumber - 1, modifiedLineRange.endLineNumberExclusive - 1) - ), timeout, considerWhitespaceChanges); - const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, originalLines, modifiedLines, true); - moves.push(new MovedText(new SimpleLineRangeMapping(originalLineRange, modifiedLineRange), mappings)); + moves.push(new SimpleLineRangeMapping(originalLineRange, modifiedLineRange)); modifiedSet.addRange(modifiedLineRange); originalSet.addRange(originalLineRange); } } - return moves; + // join moves + moves.sort(compareBy(m => m.original.startLineNumber, numberComparator)); + if (moves.length === 0) { + return []; + } + const joinedMoves = [moves[0]]; + for (let i = 1; i < moves.length; i++) { + const last = joinedMoves[joinedMoves.length - 1]; + const current = moves[i]; + + const originalDist = current.original.startLineNumber - last.original.endLineNumberExclusive; + const modifiedDist = current.modified.startLineNumber - last.modified.endLineNumberExclusive; + const currentMoveAfterLast = originalDist >= 0 && modifiedDist >= 0; + + if (currentMoveAfterLast && originalDist <= 1 && modifiedDist <= 1) { + joinedMoves[joinedMoves.length - 1] = last.join(current); + continue; + } + + const originalText = current.original.toOffsetRange().slice(originalLines).map(l => l.trim()).join('\n'); + if (originalText.length <= 10) { + // Ignore small moves + continue; + } + joinedMoves.push(current); + } + + const fullMoves = joinedMoves.map(m => { + const moveChanges = this.refineDiff(originalLines, modifiedLines, new SequenceDiff( + m.original.toOffsetRange(), + m.modified.toOffsetRange(), + ), timeout, considerWhitespaceChanges); + const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, originalLines, modifiedLines, true); + return new MovedText(m, mappings); + }); + return fullMoves; } private refineDiff(originalLines: string[], modifiedLines: string[], diff: SequenceDiff, timeout: ITimeout, considerWhitespaceChanges: boolean): { mappings: RangeMapping[]; hitTimeout: boolean } { @@ -659,7 +688,7 @@ export class LinesSliceCharSequence implements ISequence { private readonly firstCharOffsetByLineMinusOne: number[] = []; public readonly lineRange: OffsetRange; // To account for trimming - private readonly offsetByLine: number[] = []; + private readonly additionalOffsetByLine: number[] = []; constructor(public readonly lines: string[], lineRange: OffsetRange, public readonly considerWhitespaceChanges: boolean) { // This slice has to have lineRange.length many \n! (otherwise diffing against an empty slice will be problematic) @@ -687,7 +716,7 @@ export class LinesSliceCharSequence implements ISequence { line = trimmedStartLine.trimEnd(); } - this.offsetByLine.push(offset); + this.additionalOffsetByLine.push(offset); for (let i = 0; i < line.length; i++) { this.elements.push(line.charCodeAt(i)); @@ -700,7 +729,7 @@ export class LinesSliceCharSequence implements ISequence { } } // To account for the last line - this.offsetByLine.push(0); + this.additionalOffsetByLine.push(0); } toString() { @@ -767,7 +796,7 @@ export class LinesSliceCharSequence implements ISequence { } const offsetOfFirstCharInLine = i === 0 ? 0 : this.firstCharOffsetByLineMinusOne[i - 1]; - return new Position(this.lineRange.start + i + 1, offset - offsetOfFirstCharInLine + 1 + this.offsetByLine[i]); + return new Position(this.lineRange.start + i + 1, offset - offsetOfFirstCharInLine + 1 + this.additionalOffsetByLine[i]); } public translateRange(range: OffsetRange): Range { @@ -805,9 +834,52 @@ export class LinesSliceCharSequence implements ISequence { return this.translateOffset(range.endExclusive).lineNumber - this.translateOffset(range.start).lineNumber; } - isStronglyEqual(offset1: number, offset2: number): boolean { + public isStronglyEqual(offset1: number, offset2: number): boolean { return this.elements[offset1] === this.elements[offset2]; } + + public extendToFullLines(range: OffsetRange): OffsetRange { + const firstIdx = findLastIdxMonotonous(this.firstCharOffsetByLineMinusOne, x => x <= range.start); + const lastIdx = findFirstIdxMonotonous(this.firstCharOffsetByLineMinusOne, x => range.endExclusive <= x); + + const start = firstIdx === -1 ? 0 : this.firstCharOffsetByLineMinusOne[firstIdx]; + const end = lastIdx === this.firstCharOffsetByLineMinusOne.length ? this.elements.length : this.firstCharOffsetByLineMinusOne[lastIdx]; + return new OffsetRange(start, end); + } +} + +/** + * @returns -1 if predicate is false for all items + */ +function findLastIdxMonotonous(arr: T[], predicate: (item: T) => boolean): number { + let i = 0; + let j = arr.length; + while (i < j) { + const k = Math.floor((i + j) / 2); + if (predicate(arr[k])) { + i = k + 1; + } else { + j = k; + } + } + return i - 1; +} + +/** + * @returns arr.length if predicate is false for all items + */ +function findFirstIdxMonotonous(arr: T[], predicate: (item: T) => boolean): number { + let i = 0; + let j = arr.length; + while (i < j) { + const k = Math.floor((i + j) / 2); + if (predicate(arr[k])) { + j = k; + } else { + i = k + 1; + } + } + return i; } function isWordChar(charCode: number): boolean {