mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 13:00:58 +00:00
Introduces diffing infrastructure & experimental diffing algorithm. (#157646)
* Introduces diffing infrastructure & experimental diffing algorithm. * Fixes CI * Fixes unit test * Fixes CI tests.
This commit is contained in:
parent
8bf82819fc
commit
516f0d1246
|
@ -72,7 +72,7 @@ export interface ICommandHandler {
|
|||
#include(vs/editor/common/core/editOperation): ISingleEditOperation
|
||||
#include(vs/editor/common/core/wordHelper): IWordAtPosition
|
||||
#includeAll(vs/editor/common/model): IScrollEvent
|
||||
#include(vs/editor/common/diff/diffComputer): IChange, ICharChange, ILineChange
|
||||
#include(vs/editor/common/diff/smartLinesDiffComputer): IChange, ICharChange, ILineChange
|
||||
#include(vs/editor/common/core/dimension): IDimension
|
||||
#includeAll(vs/editor/common/editorCommon): IScrollEvent
|
||||
#includeAll(vs/editor/common/textModelEvents):
|
||||
|
|
|
@ -19,7 +19,7 @@ import { OverviewRulerZone } from 'vs/editor/common/viewModel/overviewZoneManage
|
|||
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IEditorWhitespace, IViewModel } from 'vs/editor/common/viewModel';
|
||||
import { InjectedText } from 'vs/editor/common/modelLineProjectionData';
|
||||
import { IDiffComputationResult, ILineChange } from 'vs/editor/common/diff/diffComputer';
|
||||
import { ILineChange, IDiffComputationResult } from 'vs/editor/common/diff/smartLinesDiffComputer';
|
||||
import { IDimension } from 'vs/editor/common/core/dimension';
|
||||
|
||||
/**
|
||||
|
|
|
@ -10,12 +10,11 @@ import { SimpleWorkerClient, logOnceWebWorkerWarning, IWorkerClient } from 'vs/b
|
|||
import { DefaultWorkerFactory } from 'vs/base/browser/defaultWorkerFactory';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { IRange, Range } from 'vs/editor/common/core/range';
|
||||
import { IChange, IDiffComputationResult } from 'vs/editor/common/diff/diffComputer';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import * as languages from 'vs/editor/common/languages';
|
||||
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
|
||||
import { EditorSimpleWorker } from 'vs/editor/common/services/editorSimpleWorker';
|
||||
import { IEditorWorkerService, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorker';
|
||||
import { IDiffComputationResult, IEditorWorkerService, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorker';
|
||||
import { IModelService } from 'vs/editor/common/services/model';
|
||||
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration';
|
||||
import { regExpFlags } from 'vs/base/common/strings';
|
||||
|
@ -26,6 +25,8 @@ import { canceled } from 'vs/base/common/errors';
|
|||
import { UnicodeHighlighterOptions } from 'vs/editor/common/services/unicodeTextModelHighlighter';
|
||||
import { IEditorWorkerHost } from 'vs/editor/common/services/editorWorkerHost';
|
||||
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
|
||||
import { IChange } from 'vs/editor/common/diff/smartLinesDiffComputer';
|
||||
import { IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider';
|
||||
|
||||
/**
|
||||
* Stop syncing a model to the worker if it was not needed for 1 min.
|
||||
|
@ -94,8 +95,8 @@ export class EditorWorkerService extends Disposable implements IEditorWorkerServ
|
|||
return this._workerManager.withWorker().then(client => client.computedUnicodeHighlights(uri, options, range));
|
||||
}
|
||||
|
||||
public computeDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean, maxComputationTime: number): Promise<IDiffComputationResult | null> {
|
||||
return this._workerManager.withWorker().then(client => client.computeDiff(original, modified, ignoreTrimWhitespace, maxComputationTime));
|
||||
public computeDiff(original: URI, modified: URI, options: IDocumentDiffProviderOptions): Promise<IDiffComputationResult | null> {
|
||||
return this._workerManager.withWorker().then(client => client.computeDiff(original, modified, options));
|
||||
}
|
||||
|
||||
public canComputeDirtyDiff(original: URI, modified: URI): boolean {
|
||||
|
@ -491,9 +492,9 @@ export class EditorWorkerClient extends Disposable implements IEditorWorkerClien
|
|||
});
|
||||
}
|
||||
|
||||
public computeDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean, maxComputationTime: number): Promise<IDiffComputationResult | null> {
|
||||
public computeDiff(original: URI, modified: URI, options: IDocumentDiffProviderOptions): Promise<IDiffComputationResult | null> {
|
||||
return this._withSyncedResources([original, modified], /* forceLargeModels */true).then(proxy => {
|
||||
return proxy.computeDiff(original.toString(), modified.toString(), ignoreTrimWhitespace, maxComputationTime);
|
||||
return proxy.computeDiff(original.toString(), modified.toString(), options);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -53,10 +53,12 @@ import { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens';
|
|||
import { FontInfo } from 'vs/editor/common/config/fontInfo';
|
||||
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
|
||||
import { ILineBreaksComputer } from 'vs/editor/common/modelLineProjectionData';
|
||||
import { IChange, ICharChange, IDiffComputationResult, ILineChange } from 'vs/editor/common/diff/diffComputer';
|
||||
import { IChange, ICharChange, IDiffComputationResult, ILineChange } from 'vs/editor/common/diff/smartLinesDiffComputer';
|
||||
import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration';
|
||||
import { IDimension } from 'vs/editor/common/core/dimension';
|
||||
import { isHighContrast } from 'vs/platform/theme/common/theme';
|
||||
import { IDocumentDiffProvider } from 'vs/editor/common/diff/documentDiffProvider';
|
||||
import { WorkerBasedDocumentDiffProvider } from 'vs/editor/browser/widget/workerBasedDocumentDiffProvider';
|
||||
|
||||
export interface IDiffCodeEditorWidgetOptions {
|
||||
originalEditor?: ICodeEditorWidgetOptions;
|
||||
|
@ -221,7 +223,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE
|
|||
|
||||
private readonly _updateDecorationsRunner: RunOnceScheduler;
|
||||
|
||||
private readonly _editorWorkerService: IEditorWorkerService;
|
||||
private readonly _documentDiffProvider: IDocumentDiffProvider;
|
||||
private readonly _contextKeyService: IContextKeyService;
|
||||
private readonly _instantiationService: IInstantiationService;
|
||||
private readonly _codeEditorService: ICodeEditorService;
|
||||
|
@ -246,7 +248,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE
|
|||
) {
|
||||
super();
|
||||
|
||||
this._editorWorkerService = editorWorkerService;
|
||||
this._documentDiffProvider = new WorkerBasedDocumentDiffProvider(editorWorkerService);
|
||||
this._codeEditorService = codeEditorService;
|
||||
this._contextKeyService = this._register(contextKeyService.createScoped(domElement));
|
||||
this._instantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, this._contextKeyService]));
|
||||
|
@ -272,7 +274,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE
|
|||
originalEditable: false,
|
||||
diffCodeLens: false,
|
||||
renderOverviewRuler: true,
|
||||
diffWordWrap: 'inherit'
|
||||
diffWordWrap: 'inherit',
|
||||
diffAlgorithm: 'smart',
|
||||
});
|
||||
|
||||
if (typeof options.isInEmbeddedEditor !== 'undefined') {
|
||||
|
@ -751,7 +754,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE
|
|||
this._options = newOptions;
|
||||
|
||||
const beginUpdateDecorations = (changed.ignoreTrimWhitespace || changed.renderIndicators || changed.renderMarginRevertIcon);
|
||||
const beginUpdateDecorationsSoon = (this._isVisible && (changed.maxComputationTime || changed.maxFileSize));
|
||||
const beginUpdateDecorationsSoon = (this._isVisible && (changed.maxComputationTime || changed.maxFileSize || changed.diffAlgorithm));
|
||||
|
||||
if (beginUpdateDecorations) {
|
||||
this._beginUpdateDecorations();
|
||||
|
@ -1111,13 +1114,65 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE
|
|||
}
|
||||
|
||||
this._setState(editorBrowser.DiffEditorState.ComputingDiff);
|
||||
this._editorWorkerService.computeDiff(currentOriginalModel.uri, currentModifiedModel.uri, this._options.ignoreTrimWhitespace, this._options.maxComputationTime).then((result) => {
|
||||
this._documentDiffProvider.computeDiff(currentOriginalModel, currentModifiedModel, {
|
||||
ignoreTrimWhitespace: this._options.ignoreTrimWhitespace,
|
||||
maxComputationTime: this._options.maxComputationTime,
|
||||
diffAlgorithm: this._options.diffAlgorithm,
|
||||
}).then(result => {
|
||||
if (currentToken === this._diffComputationToken
|
||||
&& currentOriginalModel === this._originalEditor.getModel()
|
||||
&& currentModifiedModel === this._modifiedEditor.getModel()
|
||||
) {
|
||||
this._setState(editorBrowser.DiffEditorState.DiffComputed);
|
||||
this._diffComputationResult = result;
|
||||
this._diffComputationResult = {
|
||||
identical: result.identical,
|
||||
quitEarly: result.quitEarly,
|
||||
changes: result.changes.map(m => {
|
||||
// TODO don't do this translation, but use the diff result directly
|
||||
let originalStartLineNumber: number;
|
||||
let originalEndLineNumber: number;
|
||||
let modifiedStartLineNumber: number;
|
||||
let modifiedEndLineNumber: number;
|
||||
let innerChanges = m.innerChanges;
|
||||
|
||||
if (m.originalRange.isEmpty) {
|
||||
// Insertion
|
||||
originalStartLineNumber = m.originalRange.startLineNumber - 1;
|
||||
originalEndLineNumber = 0;
|
||||
innerChanges = undefined;
|
||||
} else {
|
||||
originalStartLineNumber = m.originalRange.startLineNumber;
|
||||
originalEndLineNumber = m.originalRange.endLineNumberExclusive - 1;
|
||||
}
|
||||
|
||||
if (m.modifiedRange.isEmpty) {
|
||||
// Deletion
|
||||
modifiedStartLineNumber = m.modifiedRange.startLineNumber - 1;
|
||||
modifiedEndLineNumber = 0;
|
||||
innerChanges = undefined;
|
||||
} else {
|
||||
modifiedStartLineNumber = m.modifiedRange.startLineNumber;
|
||||
modifiedEndLineNumber = m.modifiedRange.endLineNumberExclusive - 1;
|
||||
}
|
||||
|
||||
return {
|
||||
originalStartLineNumber,
|
||||
originalEndLineNumber,
|
||||
modifiedStartLineNumber,
|
||||
modifiedEndLineNumber,
|
||||
charChanges: innerChanges?.map(m => ({
|
||||
originalStartLineNumber: m.originalRange.startLineNumber,
|
||||
originalStartColumn: m.originalRange.startColumn,
|
||||
originalEndLineNumber: m.originalRange.endLineNumber,
|
||||
originalEndColumn: m.originalRange.endColumn,
|
||||
modifiedStartLineNumber: m.modifiedRange.startLineNumber,
|
||||
modifiedStartColumn: m.modifiedRange.startColumn,
|
||||
modifiedEndLineNumber: m.modifiedRange.endLineNumber,
|
||||
modifiedEndColumn: m.modifiedRange.endColumn,
|
||||
}))
|
||||
};
|
||||
})
|
||||
};
|
||||
this._updateDecorationsRunner.schedule();
|
||||
this._onDidUpdateDiff.fire();
|
||||
}
|
||||
|
@ -2655,6 +2710,7 @@ function validateDiffEditorOptions(options: Readonly<IDiffEditorOptions>, defaul
|
|||
diffCodeLens: validateBooleanOption(options.diffCodeLens, defaults.diffCodeLens),
|
||||
renderOverviewRuler: validateBooleanOption(options.renderOverviewRuler, defaults.renderOverviewRuler),
|
||||
diffWordWrap: validateDiffWordWrap(options.diffWordWrap, defaults.diffWordWrap),
|
||||
diffAlgorithm: validateStringSetOption(options.diffAlgorithm, defaults.diffAlgorithm, ['smart', 'experimental']),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -2671,6 +2727,7 @@ function changedDiffEditorOptions(a: ValidDiffEditorBaseOptions, b: ValidDiffEdi
|
|||
diffCodeLens: (a.diffCodeLens !== b.diffCodeLens),
|
||||
renderOverviewRuler: (a.renderOverviewRuler !== b.renderOverviewRuler),
|
||||
diffWordWrap: (a.diffWordWrap !== b.diffWordWrap),
|
||||
diffAlgorithm: (a.diffAlgorithm !== b.diffAlgorithm),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import * as objects from 'vs/base/common/objects';
|
|||
import { IDiffEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { ICursorPositionChangedEvent } from 'vs/editor/common/cursorEvents';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ILineChange } from 'vs/editor/common/diff/diffComputer';
|
||||
import { ILineChange } from 'vs/editor/common/diff/smartLinesDiffComputer';
|
||||
import { ScrollType } from 'vs/editor/common/editorCommon';
|
||||
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ import { Codicon } from 'vs/base/common/codicons';
|
|||
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
|
||||
import { ILanguageIdCodec } from 'vs/editor/common/languages';
|
||||
import { ILanguageService } from 'vs/editor/common/languages/language';
|
||||
import { ILineChange } from 'vs/editor/common/diff/diffComputer';
|
||||
import { ILineChange } from 'vs/editor/common/diff/smartLinesDiffComputer';
|
||||
|
||||
const DIFF_LINES_PADDING = 3;
|
||||
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { LineRange, LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { IDocumentDiff, IDocumentDiffProvider, IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider';
|
||||
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
|
||||
export class WorkerBasedDocumentDiffProvider implements IDocumentDiffProvider {
|
||||
constructor(
|
||||
@IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService,
|
||||
) {
|
||||
}
|
||||
|
||||
async computeDiff(original: ITextModel, modified: ITextModel, options: IDocumentDiffProviderOptions): Promise<IDocumentDiff> {
|
||||
const result = await this.editorWorkerService.computeDiff(original.uri, modified.uri, options);
|
||||
if (!result) {
|
||||
throw new Error('no diff result available');
|
||||
}
|
||||
|
||||
// Convert from space efficient JSON data to rich objects.
|
||||
return {
|
||||
identical: result.identical,
|
||||
quitEarly: result.quitEarly,
|
||||
changes: result.changes.map(
|
||||
(c) =>
|
||||
new LineRangeMapping(
|
||||
new LineRange(c[0], c[1]),
|
||||
new LineRange(c[2], c[3]),
|
||||
c[4]?.map(
|
||||
(c) =>
|
||||
new RangeMapping(
|
||||
new Range(c[0], c[1], c[2], c[3]),
|
||||
new Range(c[4], c[5], c[6], c[7])
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -175,7 +175,16 @@ const editorConfiguration: IConfigurationNode = {
|
|||
nls.localize('wordWrap.on', "Lines will wrap at the viewport width."),
|
||||
nls.localize('wordWrap.inherit', "Lines will wrap according to the `#editor.wordWrap#` setting."),
|
||||
]
|
||||
}
|
||||
},
|
||||
'diffEditor.diffAlgorithm': {
|
||||
type: 'string',
|
||||
enum: ['smart', 'experimental'],
|
||||
default: 'smart',
|
||||
markdownEnumDescriptions: [
|
||||
nls.localize('diffAlgorithm.smart', "Uses the default diffing algorithm."),
|
||||
nls.localize('diffAlgorithm.experimental', "Uses an experimental diffing algorithm."),
|
||||
]
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -733,6 +733,10 @@ export interface IDiffEditorBaseOptions {
|
|||
* Control the wrapping of the diff editor.
|
||||
*/
|
||||
diffWordWrap?: 'off' | 'on' | 'inherit';
|
||||
/**
|
||||
* Diff Algorithm
|
||||
*/
|
||||
diffAlgorithm?: 'smart' | 'experimental';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -333,6 +333,13 @@ export class Range {
|
|||
return Range.collapseToStart(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the range by the given amount of lines.
|
||||
*/
|
||||
public delta(lineCount: number): Range {
|
||||
return new Range(this.startLineNumber + lineCount, this.startColumn, this.endLineNumber + lineCount, this.endColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new empty range using this range's start position.
|
||||
*/
|
||||
|
|
54
src/vs/editor/common/diff/algorithms/diffAlgorithm.ts
Normal file
54
src/vs/editor/common/diff/algorithms/diffAlgorithm.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Represents a synchronous diff algorithm. Should be executed in a worker.
|
||||
*/
|
||||
export interface IDiffAlgorithm {
|
||||
compute(sequence1: ISequence, sequence2: ISequence): SequenceDiff[];
|
||||
}
|
||||
|
||||
export class SequenceDiff {
|
||||
constructor(
|
||||
public readonly seq1Range: OffsetRange,
|
||||
public readonly seq2Range: OffsetRange
|
||||
) { }
|
||||
}
|
||||
|
||||
/**
|
||||
* Todo move this class to some top level utils.
|
||||
*/
|
||||
export class OffsetRange {
|
||||
constructor(public readonly start: number, public readonly endExclusive: number) { }
|
||||
|
||||
get isEmpty(): boolean {
|
||||
return this.start === this.endExclusive;
|
||||
}
|
||||
|
||||
public delta(offset: number): OffsetRange {
|
||||
return new OffsetRange(this.start + offset, this.endExclusive + offset);
|
||||
}
|
||||
|
||||
public get length(): number {
|
||||
return this.endExclusive - this.start;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ISequence {
|
||||
getElement(offset: number): number;
|
||||
get length(): number;
|
||||
}
|
||||
|
||||
export class SequenceFromIntArray implements ISequence {
|
||||
constructor(private readonly arr: number[]) { }
|
||||
|
||||
getElement(offset: number): number {
|
||||
return this.arr[offset];
|
||||
}
|
||||
|
||||
get length(): number {
|
||||
return this.arr.length;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IDiffAlgorithm, SequenceDiff, OffsetRange, ISequence } from 'vs/editor/common/diff/algorithms/diffAlgorithm';
|
||||
import { Array2D } from 'vs/editor/common/diff/algorithms/utils';
|
||||
|
||||
/**
|
||||
* A O(MN) diffing algorithm that supports a score function.
|
||||
* The algorithm can be improved by processing the 2d array diagonally.
|
||||
*/
|
||||
export class DynamicProgrammingDiffing implements IDiffAlgorithm {
|
||||
compute(sequence1: ISequence, sequence2: ISequence, equalityScore?: (offset1: number, offset2: number) => number): SequenceDiff[] {
|
||||
/**
|
||||
* lcsLengths.get(i, j): Length of the longest common subsequence of sequence1.substring(0, i + 1) and sequence2.substring(0, j + 1).
|
||||
*/
|
||||
const lcsLengths = new Array2D<number>(sequence1.length, sequence2.length);
|
||||
const directions = new Array2D<number>(sequence1.length, sequence2.length);
|
||||
const lengths = new Array2D<number>(sequence1.length, sequence2.length);
|
||||
|
||||
// ==== Initializing lcsLengths ====
|
||||
for (let s1 = 0; s1 < sequence1.length; s1++) {
|
||||
for (let s2 = 0; s2 < sequence2.length; s2++) {
|
||||
const horizontalLen = s1 === 0 ? 0 : lcsLengths.get(s1 - 1, s2);
|
||||
const verticalLen = s2 === 0 ? 0 : lcsLengths.get(s1, s2 - 1);
|
||||
|
||||
let extendedSeqScore: number;
|
||||
if (sequence1.getElement(s1) === sequence2.getElement(s2)) {
|
||||
if (s1 === 0 || s2 === 0) {
|
||||
extendedSeqScore = 0;
|
||||
} else {
|
||||
extendedSeqScore = lcsLengths.get(s1 - 1, s2 - 1);
|
||||
}
|
||||
extendedSeqScore += (equalityScore ? equalityScore(s1, s2) : 1);
|
||||
} else {
|
||||
extendedSeqScore = -1;
|
||||
}
|
||||
|
||||
const newValue = Math.max(horizontalLen, verticalLen, extendedSeqScore);
|
||||
|
||||
if (newValue === horizontalLen) {
|
||||
lengths.set(s1, s2, 0);
|
||||
directions.set(s1, s2, 1);
|
||||
} else if (newValue === verticalLen) {
|
||||
lengths.set(s1, s2, 0);
|
||||
directions.set(s1, s2, 2);
|
||||
} else {
|
||||
const prevLen = s1 > 0 && s2 > 0 ? lengths.get(s1 - 1, s2 - 1) : 0;
|
||||
lengths.set(s1, s2, prevLen + 1);
|
||||
directions.set(s1, s2, 3);
|
||||
}
|
||||
|
||||
lcsLengths.set(s1, s2, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
// ==== Backtracking ====
|
||||
const result: SequenceDiff[] = [];
|
||||
let lastAligningPosS1: number = sequence1.length;
|
||||
let lastAligningPosS2: number = sequence2.length;
|
||||
|
||||
function reportDecreasingAligningPositions(s1: number, s2: number): void {
|
||||
if (s1 + 1 !== lastAligningPosS1 || s2 + 1 !== lastAligningPosS2) {
|
||||
result.push(new SequenceDiff(
|
||||
new OffsetRange(s1 + 1, lastAligningPosS1),
|
||||
new OffsetRange(s2 + 1, lastAligningPosS2),
|
||||
));
|
||||
}
|
||||
lastAligningPosS1 = s1;
|
||||
lastAligningPosS2 = s2;
|
||||
}
|
||||
|
||||
let s1 = sequence1.length - 1;
|
||||
let s2 = sequence2.length - 1;
|
||||
while (s1 >= 0 && s2 >= 0) {
|
||||
if (directions.get(s1, s2) === 3) {
|
||||
reportDecreasingAligningPositions(s1, s2);
|
||||
s1--;
|
||||
s2--;
|
||||
} else {
|
||||
if (directions.get(s1, s2) === 1) {
|
||||
s1--;
|
||||
} else {
|
||||
s2--;
|
||||
}
|
||||
}
|
||||
}
|
||||
reportDecreasingAligningPositions(-1, -1);
|
||||
result.reverse();
|
||||
return result;
|
||||
}
|
||||
}
|
150
src/vs/editor/common/diff/algorithms/myersDiffAlgorithm.ts
Normal file
150
src/vs/editor/common/diff/algorithms/myersDiffAlgorithm.ts
Normal file
|
@ -0,0 +1,150 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IDiffAlgorithm, ISequence, SequenceDiff, OffsetRange } from 'vs/editor/common/diff/algorithms/diffAlgorithm';
|
||||
|
||||
/**
|
||||
* An O(ND) diff algorithm that has a quadratic space worst-case complexity.
|
||||
*/
|
||||
export class MyersDiffAlgorithm implements IDiffAlgorithm {
|
||||
compute(seq1: ISequence, seq2: ISequence): SequenceDiff[] {
|
||||
function getXAfterSnake(x: number, y: number): number {
|
||||
while (x < seq1.length && y < seq2.length && seq1.getElement(x) === seq2.getElement(y)) {
|
||||
x++;
|
||||
y++;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
let d = 0;
|
||||
// V[k]: X value of longest d-line that ends in diagonal k.
|
||||
// d-line: path from (0,0) to (x,y) that uses exactly d non-diagonals.
|
||||
// diagonal k: Set of points (x,y) with x-y = k.
|
||||
const V = new FastInt32Array();
|
||||
V.set(0, getXAfterSnake(0, 0));
|
||||
|
||||
const paths = new FastArrayNegativeIndices<SnakePath | null>();
|
||||
paths.set(0, V.get(0) === 0 ? null : new SnakePath(null, 0, 0, V.get(0)));
|
||||
|
||||
let k = 0;
|
||||
|
||||
loop: while (true) {
|
||||
d++;
|
||||
for (k = -d; k <= d; k += 2) {
|
||||
const maxXofDLineTop = k === d ? -1 : V.get(k + 1); // We take a vertical non-diagonal
|
||||
const maxXofDLineLeft = k === -d ? -1 : V.get(k - 1) + 1; // We take a horizontal non-diagonal (+1 x)
|
||||
const x = Math.min(Math.max(maxXofDLineTop, maxXofDLineLeft), seq1.length);
|
||||
const y = x - k;
|
||||
const newMaxX = getXAfterSnake(x, y);
|
||||
V.set(k, newMaxX);
|
||||
const lastPath = x === maxXofDLineTop ? paths.get(k + 1) : paths.get(k - 1);
|
||||
paths.set(k, newMaxX !== x ? new SnakePath(lastPath, x, y, newMaxX - x) : lastPath);
|
||||
|
||||
if (V.get(k) === seq1.length && V.get(k) - k === seq2.length) {
|
||||
break loop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let path = paths.get(k);
|
||||
const result: SequenceDiff[] = [];
|
||||
let lastAligningPosS1: number = seq1.length;
|
||||
let lastAligningPosS2: number = seq2.length;
|
||||
|
||||
while (true) {
|
||||
const endX = path ? path.x + path.length : 0;
|
||||
const endY = path ? path.y + path.length : 0;
|
||||
|
||||
if (endX !== lastAligningPosS1 || endY !== lastAligningPosS2) {
|
||||
result.push(new SequenceDiff(
|
||||
new OffsetRange(endX, lastAligningPosS1),
|
||||
new OffsetRange(endY, lastAligningPosS2),
|
||||
));
|
||||
}
|
||||
if (!path) {
|
||||
break;
|
||||
}
|
||||
lastAligningPosS1 = path.x;
|
||||
lastAligningPosS2 = path.y;
|
||||
|
||||
path = path.prev;
|
||||
}
|
||||
|
||||
result.reverse();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class SnakePath {
|
||||
constructor(
|
||||
public readonly prev: SnakePath | null,
|
||||
public readonly x: number,
|
||||
public readonly y: number,
|
||||
public readonly length: number
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An array that supports fast negative indices.
|
||||
*/
|
||||
class FastInt32Array {
|
||||
private positiveArr: Int32Array = new Int32Array(10);
|
||||
private negativeArr: Int32Array = new Int32Array(10);
|
||||
|
||||
get(idx: number): number {
|
||||
if (idx < 0) {
|
||||
idx = -idx - 1;
|
||||
return this.negativeArr[idx];
|
||||
} else {
|
||||
return this.positiveArr[idx];
|
||||
}
|
||||
}
|
||||
|
||||
set(idx: number, value: number): void {
|
||||
if (idx < 0) {
|
||||
idx = -idx - 1;
|
||||
if (idx > this.negativeArr.length) {
|
||||
const arr = this.negativeArr;
|
||||
this.negativeArr = new Int32Array(arr.length * 2);
|
||||
this.negativeArr.set(arr);
|
||||
}
|
||||
this.negativeArr[idx] = value;
|
||||
} else {
|
||||
if (idx > this.positiveArr.length) {
|
||||
const arr = this.positiveArr;
|
||||
this.positiveArr = new Int32Array(arr.length * 2);
|
||||
this.positiveArr.set(arr);
|
||||
}
|
||||
this.positiveArr[idx] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An array that supports fast negative indices.
|
||||
*/
|
||||
class FastArrayNegativeIndices<T> {
|
||||
private readonly positiveArr: T[] = [];
|
||||
private readonly negativeArr: T[] = [];
|
||||
|
||||
get(idx: number): T {
|
||||
if (idx < 0) {
|
||||
idx = -idx - 1;
|
||||
return this.negativeArr[idx];
|
||||
} else {
|
||||
return this.positiveArr[idx];
|
||||
}
|
||||
}
|
||||
|
||||
set(idx: number, value: T): void {
|
||||
if (idx < 0) {
|
||||
idx = -idx - 1;
|
||||
this.negativeArr[idx] = value;
|
||||
} else {
|
||||
this.positiveArr[idx] = value;
|
||||
}
|
||||
}
|
||||
}
|
20
src/vs/editor/common/diff/algorithms/utils.ts
Normal file
20
src/vs/editor/common/diff/algorithms/utils.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export class Array2D<T> {
|
||||
private readonly array: T[] = [];
|
||||
|
||||
constructor(public readonly width: number, public readonly height: number) {
|
||||
this.array = new Array<T>(width * height);
|
||||
}
|
||||
|
||||
get(x: number, y: number): T {
|
||||
return this.array[x + y * this.width];
|
||||
}
|
||||
|
||||
set(x: number, y: number, value: T): void {
|
||||
this.array[x + y * this.width] = value;
|
||||
}
|
||||
}
|
23
src/vs/editor/common/diff/documentDiffProvider.ts
Normal file
23
src/vs/editor/common/diff/documentDiffProvider.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
|
||||
export interface IDocumentDiffProvider {
|
||||
computeDiff(original: ITextModel, modified: ITextModel, options: IDocumentDiffProviderOptions): Promise<IDocumentDiff>;
|
||||
}
|
||||
|
||||
export interface IDocumentDiffProviderOptions {
|
||||
ignoreTrimWhitespace: boolean;
|
||||
maxComputationTime: number;
|
||||
diffAlgorithm: 'smart' | 'experimental';
|
||||
}
|
||||
|
||||
export interface IDocumentDiff {
|
||||
readonly identical: boolean;
|
||||
readonly quitEarly: boolean;
|
||||
readonly changes: LineRangeMapping[];
|
||||
}
|
69
src/vs/editor/common/diff/linesDiffComputer.ts
Normal file
69
src/vs/editor/common/diff/linesDiffComputer.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
|
||||
export interface ILinesDiffComputer {
|
||||
computeDiff(originalLines: string[], modifiedLines: string[], options: ILinesDiffComputerOptions): ILinesDiff;
|
||||
}
|
||||
|
||||
export interface ILinesDiffComputerOptions {
|
||||
ignoreTrimWhitespace: boolean;
|
||||
maxComputationTime: number;
|
||||
}
|
||||
|
||||
export interface ILinesDiff {
|
||||
readonly quitEarly: boolean;
|
||||
readonly changes: LineRangeMapping[];
|
||||
}
|
||||
|
||||
export class LineRangeMapping {
|
||||
constructor(
|
||||
readonly originalRange: LineRange,
|
||||
readonly modifiedRange: LineRange,
|
||||
/**
|
||||
* Meaning of `undefined` unclear.
|
||||
*/
|
||||
readonly innerChanges: RangeMapping[] | undefined,
|
||||
) { }
|
||||
|
||||
toString(): string {
|
||||
return `{${this.originalRange.toString()}->${this.modifiedRange.toString()}}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class RangeMapping {
|
||||
constructor(
|
||||
readonly originalRange: Range,
|
||||
readonly modifiedRange: Range,
|
||||
) { }
|
||||
|
||||
toString(): string {
|
||||
return `{${this.originalRange.toString()}->${this.modifiedRange.toString()}}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1-based.
|
||||
*/
|
||||
export class LineRange {
|
||||
constructor(public readonly startLineNumber: number, public readonly endLineNumberExclusive: number) { }
|
||||
|
||||
get isEmpty(): boolean {
|
||||
return this.startLineNumber === this.endLineNumberExclusive;
|
||||
}
|
||||
|
||||
public delta(offset: number): LineRange {
|
||||
return new LineRange(this.startLineNumber + offset, this.endLineNumberExclusive + offset);
|
||||
}
|
||||
|
||||
public get length(): number {
|
||||
return this.endLineNumberExclusive - this.startLineNumber;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `[${this.startLineNumber},${this.endLineNumberExclusive})`;
|
||||
}
|
||||
}
|
13
src/vs/editor/common/diff/linesDiffComputers.ts
Normal file
13
src/vs/editor/common/diff/linesDiffComputers.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { MyersDiffAlgorithm } from 'vs/editor/common/diff/algorithms/myersDiffAlgorithm';
|
||||
import { SmartLinesDiffComputer } from 'vs/editor/common/diff/smartLinesDiffComputer';
|
||||
import { StandardLinesDiffComputer } from 'vs/editor/common/diff/standardLinesDiffComputer';
|
||||
|
||||
export const linesDiffComputers = {
|
||||
smart: new SmartLinesDiffComputer(),
|
||||
experimental: new StandardLinesDiffComputer(new MyersDiffAlgorithm())
|
||||
};
|
|
@ -5,10 +5,54 @@
|
|||
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
import { IDiffChange, ISequence, LcsDiff, IDiffResult } from 'vs/base/common/diff/diff';
|
||||
import { ILinesDiffComputer, ILinesDiff, ILinesDiffComputerOptions, LineRange, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
|
||||
const MINIMUM_MATCHING_CHARACTER_LENGTH = 3;
|
||||
|
||||
export class SmartLinesDiffComputer implements ILinesDiffComputer {
|
||||
computeDiff(originalLines: string[], modifiedLines: string[], options: ILinesDiffComputerOptions): ILinesDiff {
|
||||
const diffComputer = new DiffComputer(originalLines, modifiedLines, {
|
||||
maxComputationTime: options.maxComputationTime,
|
||||
shouldIgnoreTrimWhitespace: options.ignoreTrimWhitespace,
|
||||
shouldComputeCharChanges: true,
|
||||
shouldMakePrettyDiff: true,
|
||||
shouldPostProcessCharChanges: true,
|
||||
});
|
||||
const result = diffComputer.computeDiff();
|
||||
return {
|
||||
quitEarly: result.quitEarly,
|
||||
changes: result.changes.map(c => {
|
||||
let originalRange: LineRange;
|
||||
if (c.originalEndLineNumber === 0) {
|
||||
// Insertion
|
||||
originalRange = new LineRange(c.originalStartLineNumber + 1, c.originalStartLineNumber + 1);
|
||||
} else {
|
||||
originalRange = new LineRange(c.originalStartLineNumber, c.originalEndLineNumber + 1);
|
||||
}
|
||||
|
||||
let modifiedRange: LineRange;
|
||||
if (c.modifiedEndLineNumber === 0) {
|
||||
// Deletion
|
||||
modifiedRange = new LineRange(c.modifiedStartLineNumber + 1, c.modifiedStartLineNumber + 1);
|
||||
} else {
|
||||
modifiedRange = new LineRange(c.modifiedStartLineNumber, c.modifiedEndLineNumber + 1);
|
||||
}
|
||||
|
||||
return {
|
||||
originalRange,
|
||||
modifiedRange,
|
||||
innerChanges: c.charChanges?.map(c => new RangeMapping(
|
||||
new Range(c.originalStartLineNumber, c.originalStartColumn, c.originalEndLineNumber, c.originalEndColumn),
|
||||
new Range(c.modifiedStartLineNumber, c.modifiedStartColumn, c.modifiedEndLineNumber, c.modifiedEndColumn),
|
||||
))
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface IDiffComputationResult {
|
||||
quitEarly: boolean;
|
||||
identical: boolean;
|
||||
|
@ -395,16 +439,7 @@ export class DiffComputer {
|
|||
originalEndLineNumber: 1,
|
||||
modifiedStartLineNumber: 1,
|
||||
modifiedEndLineNumber: this.modified.lines.length,
|
||||
charChanges: [{
|
||||
modifiedEndColumn: 0,
|
||||
modifiedEndLineNumber: 0,
|
||||
modifiedStartColumn: 0,
|
||||
modifiedStartLineNumber: 0,
|
||||
originalEndColumn: 0,
|
||||
originalEndLineNumber: 0,
|
||||
originalStartColumn: 0,
|
||||
originalStartLineNumber: 0
|
||||
}]
|
||||
charChanges: undefined
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
@ -418,16 +453,7 @@ export class DiffComputer {
|
|||
originalEndLineNumber: this.original.lines.length,
|
||||
modifiedStartLineNumber: 1,
|
||||
modifiedEndLineNumber: 1,
|
||||
charChanges: [{
|
||||
modifiedEndColumn: 0,
|
||||
modifiedEndLineNumber: 0,
|
||||
modifiedStartColumn: 0,
|
||||
modifiedStartLineNumber: 0,
|
||||
originalEndColumn: 0,
|
||||
originalEndLineNumber: 0,
|
||||
originalStartColumn: 0,
|
||||
originalStartLineNumber: 0
|
||||
}]
|
||||
charChanges: undefined
|
||||
}]
|
||||
};
|
||||
}
|
196
src/vs/editor/common/diff/standardLinesDiffComputer.ts
Normal file
196
src/vs/editor/common/diff/standardLinesDiffComputer.ts
Normal file
|
@ -0,0 +1,196 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { IDiffAlgorithm, SequenceFromIntArray, OffsetRange, SequenceDiff, ISequence } from 'vs/editor/common/diff/algorithms/diffAlgorithm';
|
||||
import { DynamicProgrammingDiffing } from 'vs/editor/common/diff/algorithms/dynamicProgrammingDiffing';
|
||||
import { ILinesDiff, ILinesDiffComputer, ILinesDiffComputerOptions, LineRange, LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
|
||||
|
||||
export class StandardLinesDiffComputer implements ILinesDiffComputer {
|
||||
private readonly lineDiffingAlgorithm = new DynamicProgrammingDiffing();
|
||||
|
||||
constructor(
|
||||
private readonly detailedDiffingAlgorithm: IDiffAlgorithm
|
||||
) { }
|
||||
|
||||
computeDiff(originalLines: string[], modifiedLines: string[], options: ILinesDiffComputerOptions): ILinesDiff {
|
||||
const perfectHashes = new Map<string, number>();
|
||||
function getOrCreateHash(text: string): number {
|
||||
let hash = perfectHashes.get(text);
|
||||
if (hash === undefined) {
|
||||
hash = perfectHashes.size;
|
||||
perfectHashes.set(text, hash);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
const srcDocLines = originalLines.map((l) => getOrCreateHash(l.trim()));
|
||||
const tgtDocLines = modifiedLines.map((l) => getOrCreateHash(l.trim()));
|
||||
|
||||
const lineAlignments = this.lineDiffingAlgorithm.compute(
|
||||
new SequenceFromIntArray(srcDocLines),
|
||||
new SequenceFromIntArray(tgtDocLines),
|
||||
(offset1, offset2) =>
|
||||
originalLines[offset1] === modifiedLines[offset2]
|
||||
? modifiedLines[offset2].length === 0
|
||||
? 0.1
|
||||
: 1// + Math.log(1 + modifiedLines[offset2].length)
|
||||
: 0.99
|
||||
);
|
||||
|
||||
const alignments: RangeMapping[] = [];
|
||||
for (let diff of lineAlignments) {
|
||||
// Move line diffs up to improve the case of
|
||||
// AxBAzB -> AxBA(yBA)zB to
|
||||
// AxBAzB -> AxB(AyB)AzB
|
||||
if (
|
||||
(diff.seq1Range.start > 0 &&
|
||||
diff.seq1Range.length > 0 &&
|
||||
srcDocLines[diff.seq1Range.start - 1] ===
|
||||
srcDocLines[diff.seq1Range.endExclusive - 1]) ||
|
||||
(diff.seq2Range.start > 0 &&
|
||||
diff.seq2Range.length > 0 &&
|
||||
tgtDocLines[diff.seq2Range.start - 1] ===
|
||||
tgtDocLines[diff.seq2Range.endExclusive - 1])
|
||||
) {
|
||||
diff = new SequenceDiff(
|
||||
diff.seq1Range.delta(-1),
|
||||
diff.seq2Range.delta(-1),
|
||||
);
|
||||
}
|
||||
|
||||
for (const a of this.refineDiff(originalLines, modifiedLines, diff)) {
|
||||
alignments.push(a);
|
||||
}
|
||||
}
|
||||
|
||||
const changes: LineRangeMapping[] = lineRangeMappingFromRangeMappings(alignments);
|
||||
|
||||
return {
|
||||
quitEarly: false,
|
||||
changes: changes,
|
||||
};
|
||||
}
|
||||
|
||||
private refineDiff(originalLines: string[], modifiedLines: string[], diff: SequenceDiff): RangeMapping[] {
|
||||
const sourceSlice = new Slice(originalLines, diff.seq1Range);
|
||||
const targetSlice = new Slice(modifiedLines, diff.seq2Range);
|
||||
|
||||
const diffs = this.detailedDiffingAlgorithm.compute(sourceSlice, targetSlice);
|
||||
return diffs.map(
|
||||
(d) =>
|
||||
new RangeMapping(
|
||||
sourceSlice.translateRange(d.seq1Range).delta(diff.seq1Range.start),
|
||||
targetSlice.translateRange(d.seq2Range).delta(diff.seq2Range.start)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function lineRangeMappingFromRangeMappings(alignments: RangeMapping[]): LineRangeMapping[] {
|
||||
const changes: LineRangeMapping[] = [];
|
||||
for (const g of group(
|
||||
alignments,
|
||||
(a1, a2) => a2.modifiedRange.startLineNumber - (a1.modifiedRange.endLineNumber - (a1.modifiedRange.endColumn > 1 ? 0 : 1)) <= 1
|
||||
)) {
|
||||
const first = g[0];
|
||||
const last = g[g.length - 1];
|
||||
|
||||
changes.push(new LineRangeMapping(
|
||||
new LineRange(
|
||||
first.originalRange.startLineNumber,
|
||||
last.originalRange.endLineNumber + (last.originalRange.endColumn > 1 || last.modifiedRange.endColumn > 1 ? 1 : 0)
|
||||
),
|
||||
new LineRange(
|
||||
first.modifiedRange.startLineNumber,
|
||||
last.modifiedRange.endLineNumber + (last.originalRange.endColumn > 1 || last.modifiedRange.endColumn > 1 ? 1 : 0)
|
||||
),
|
||||
g
|
||||
));
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
function* group<T>(items: Iterable<T>, shouldBeGrouped: (item1: T, item2: T) => boolean): Iterable<T[]> {
|
||||
let currentGroup: T[] | undefined;
|
||||
let last: T | undefined;
|
||||
for (const item of items) {
|
||||
if (last !== undefined && shouldBeGrouped(last, item)) {
|
||||
currentGroup!.push(item);
|
||||
} else {
|
||||
if (currentGroup) {
|
||||
yield currentGroup;
|
||||
}
|
||||
currentGroup = [item];
|
||||
}
|
||||
last = item;
|
||||
}
|
||||
if (currentGroup) {
|
||||
yield currentGroup;
|
||||
}
|
||||
}
|
||||
|
||||
class Slice implements ISequence {
|
||||
private readonly elements: Int32Array;
|
||||
private readonly firstCharOnLineOffsets: Int32Array;
|
||||
|
||||
constructor(public readonly lines: string[], public readonly lineRange: OffsetRange) {
|
||||
let chars = 0;
|
||||
this.firstCharOnLineOffsets = new Int32Array(lineRange.length);
|
||||
|
||||
for (let i = lineRange.start; i < lineRange.endExclusive; i++) {
|
||||
const line = lines[i];
|
||||
chars += line.length;
|
||||
this.firstCharOnLineOffsets[i - lineRange.start] = chars + 1;
|
||||
chars++;
|
||||
}
|
||||
|
||||
this.elements = new Int32Array(chars);
|
||||
let offset = 0;
|
||||
for (let i = lineRange.start; i < lineRange.endExclusive; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
this.elements[offset + i] = line.charCodeAt(i);
|
||||
}
|
||||
offset += line.length;
|
||||
if (i < lines.length - 1) {
|
||||
this.elements[offset] = '\n'.charCodeAt(0);
|
||||
offset += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getElement(offset: number): number {
|
||||
return this.elements[offset];
|
||||
}
|
||||
|
||||
get length(): number {
|
||||
return this.elements.length;
|
||||
}
|
||||
|
||||
public translateOffset(offset: number): Position {
|
||||
// find smallest i, so that lineBreakOffsets[i] > offset using binary search
|
||||
|
||||
let i = 0;
|
||||
let j = this.firstCharOnLineOffsets.length;
|
||||
while (i < j) {
|
||||
const k = Math.floor((i + j) / 2);
|
||||
if (this.firstCharOnLineOffsets[k] > offset) {
|
||||
j = k;
|
||||
} else {
|
||||
i = k + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const offsetOfPrevLineBreak = i === 0 ? 0 : this.firstCharOnLineOffsets[i - 1];
|
||||
return new Position(i + 1, offset - offsetOfPrevLineBreak + 1);
|
||||
}
|
||||
|
||||
public translateRange(range: OffsetRange): Range {
|
||||
return Range.fromPositions(this.translateOffset(range.start), this.translateOffset(range.endExclusive));
|
||||
}
|
||||
}
|
|
@ -10,19 +10,22 @@ import { URI } from 'vs/base/common/uri';
|
|||
import { IRequestHandler } from 'vs/base/common/worker/simpleWorker';
|
||||
import { IPosition, Position } from 'vs/editor/common/core/position';
|
||||
import { IRange, Range } from 'vs/editor/common/core/range';
|
||||
import { DiffComputer, IChange, IDiffComputationResult } from 'vs/editor/common/diff/diffComputer';
|
||||
import { EndOfLineSequence, ITextModel } from 'vs/editor/common/model';
|
||||
import { IMirrorTextModel, IModelChangedEvent, MirrorTextModel as BaseMirrorModel } from 'vs/editor/common/model/mirrorTextModel';
|
||||
import { ensureValidWordDefinition, getWordAtText, IWordAtPosition } from 'vs/editor/common/core/wordHelper';
|
||||
import { IInplaceReplaceSupportResult, ILink, TextEdit } from 'vs/editor/common/languages';
|
||||
import { ILinkComputerTarget, computeLinks } from 'vs/editor/common/languages/linkComputer';
|
||||
import { BasicInplaceReplace } from 'vs/editor/common/languages/supports/inplaceReplaceSupport';
|
||||
import { IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorker';
|
||||
import { IDiffComputationResult, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorker';
|
||||
import { createMonacoBaseAPI } from 'vs/editor/common/services/editorBaseApi';
|
||||
import { IEditorWorkerHost } from 'vs/editor/common/services/editorWorkerHost';
|
||||
import { StopWatch } from 'vs/base/common/stopwatch';
|
||||
import { UnicodeTextModelHighlighter, UnicodeHighlighterOptions } from 'vs/editor/common/services/unicodeTextModelHighlighter';
|
||||
import { DiffComputer, IChange } from 'vs/editor/common/diff/smartLinesDiffComputer';
|
||||
import { ILinesDiffComputer } from 'vs/editor/common/diff/linesDiffComputer';
|
||||
import { linesDiffComputers } from 'vs/editor/common/diff/linesDiffComputers';
|
||||
import { createProxyObject, getAllMethodNames } from 'vs/base/common/objects';
|
||||
import { IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider';
|
||||
|
||||
export interface IMirrorModel extends IMirrorTextModel {
|
||||
readonly uri: URI;
|
||||
|
@ -381,33 +384,39 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable {
|
|||
|
||||
// ---- BEGIN diff --------------------------------------------------------------------------
|
||||
|
||||
public async computeDiff(originalUrl: string, modifiedUrl: string, ignoreTrimWhitespace: boolean, maxComputationTime: number): Promise<IDiffComputationResult | null> {
|
||||
public async computeDiff(originalUrl: string, modifiedUrl: string, options: IDocumentDiffProviderOptions): Promise<IDiffComputationResult | null> {
|
||||
const original = this._getModel(originalUrl);
|
||||
const modified = this._getModel(modifiedUrl);
|
||||
if (!original || !modified) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return EditorSimpleWorker.computeDiff(original, modified, ignoreTrimWhitespace, maxComputationTime);
|
||||
return EditorSimpleWorker.computeDiff(original, modified, options);
|
||||
}
|
||||
|
||||
public static computeDiff(originalTextModel: ICommonModel | ITextModel, modifiedTextModel: ICommonModel | ITextModel, ignoreTrimWhitespace: boolean, maxComputationTime: number): IDiffComputationResult | null {
|
||||
private static computeDiff(originalTextModel: ICommonModel | ITextModel, modifiedTextModel: ICommonModel | ITextModel, options: IDocumentDiffProviderOptions): IDiffComputationResult {
|
||||
const diffAlgorithm: ILinesDiffComputer = options.diffAlgorithm === 'experimental' ? linesDiffComputers.experimental : linesDiffComputers.smart;
|
||||
|
||||
const originalLines = originalTextModel.getLinesContent();
|
||||
const modifiedLines = modifiedTextModel.getLinesContent();
|
||||
const diffComputer = new DiffComputer(originalLines, modifiedLines, {
|
||||
shouldComputeCharChanges: true,
|
||||
shouldPostProcessCharChanges: true,
|
||||
shouldIgnoreTrimWhitespace: ignoreTrimWhitespace,
|
||||
shouldMakePrettyDiff: true,
|
||||
maxComputationTime: maxComputationTime
|
||||
});
|
||||
|
||||
const diffResult = diffComputer.computeDiff();
|
||||
const identical = (diffResult.changes.length > 0 ? false : this._modelsAreIdentical(originalTextModel, modifiedTextModel));
|
||||
const result = diffAlgorithm.computeDiff(originalLines, modifiedLines, options);
|
||||
|
||||
const identical = (result.changes.length > 0 ? false : this._modelsAreIdentical(originalTextModel, modifiedTextModel));
|
||||
|
||||
return {
|
||||
quitEarly: diffResult.quitEarly,
|
||||
identical: identical,
|
||||
changes: diffResult.changes
|
||||
identical,
|
||||
quitEarly: result.quitEarly,
|
||||
changes: result.changes.map(m => ([m.originalRange.startLineNumber, m.originalRange.endLineNumberExclusive, m.modifiedRange.startLineNumber, m.modifiedRange.endLineNumberExclusive, m.innerChanges?.map(m => [
|
||||
m.originalRange.startLineNumber,
|
||||
m.originalRange.startColumn,
|
||||
m.originalRange.endLineNumber,
|
||||
m.originalRange.endColumn,
|
||||
m.modifiedRange.startLineNumber,
|
||||
m.modifiedRange.startColumn,
|
||||
m.modifiedRange.endLineNumber,
|
||||
m.modifiedRange.endColumn,
|
||||
])]))
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
import { IChange, IDiffComputationResult } from 'vs/editor/common/diff/diffComputer';
|
||||
import { IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider';
|
||||
import { IChange } from 'vs/editor/common/diff/smartLinesDiffComputer';
|
||||
import { IInplaceReplaceSupportResult, TextEdit } from 'vs/editor/common/languages';
|
||||
import { UnicodeHighlighterOptions } from 'vs/editor/common/services/unicodeTextModelHighlighter';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
@ -19,7 +20,7 @@ export interface IEditorWorkerService {
|
|||
canComputeUnicodeHighlights(uri: URI): boolean;
|
||||
computedUnicodeHighlights(uri: URI, options: UnicodeHighlighterOptions, range?: IRange): Promise<IUnicodeHighlightsResult>;
|
||||
|
||||
computeDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean, maxComputationTime: number): Promise<IDiffComputationResult | null>;
|
||||
computeDiff(original: URI, modified: URI, options: IDocumentDiffProviderOptions): Promise<IDiffComputationResult | null>;
|
||||
|
||||
canComputeDirtyDiff(original: URI, modified: URI): boolean;
|
||||
computeDirtyDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean): Promise<IChange[] | null>;
|
||||
|
@ -33,6 +34,32 @@ export interface IEditorWorkerService {
|
|||
navigateValueSet(resource: URI, range: IRange, up: boolean): Promise<IInplaceReplaceSupportResult | null>;
|
||||
}
|
||||
|
||||
export interface IDiffComputationResult {
|
||||
quitEarly: boolean;
|
||||
changes: ILineChange[];
|
||||
identical: boolean;
|
||||
}
|
||||
|
||||
export type ILineChange = [
|
||||
originalStartLine: number,
|
||||
originalEndLine: number,
|
||||
modifiedStartLine: number,
|
||||
modifiedEndLine: number,
|
||||
charChanges: ICharChange[] | undefined,
|
||||
];
|
||||
|
||||
export type ICharChange = [
|
||||
originalStartLine: number,
|
||||
originalStartColumn: number,
|
||||
originalEndLine: number,
|
||||
originalEndColumn: number,
|
||||
|
||||
modifiedStartLine: number,
|
||||
modifiedStartColumn: number,
|
||||
modifiedEndLine: number,
|
||||
modifiedEndColumn: number,
|
||||
];
|
||||
|
||||
export interface IUnicodeHighlightsResult {
|
||||
ranges: IRange[];
|
||||
hasMore: boolean;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
import * as assert from 'assert';
|
||||
import { Constants } from 'vs/base/common/uint';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { DiffComputer, ICharChange, ILineChange } from 'vs/editor/common/diff/diffComputer';
|
||||
import { DiffComputer, ICharChange, ILineChange } from 'vs/editor/common/diff/smartLinesDiffComputer';
|
||||
import { IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model';
|
||||
import { createTextModel } from 'vs/editor/test/common/testTextModel';
|
||||
|
||||
|
@ -527,9 +527,7 @@ suite('Editor Diff - DiffComputer', () => {
|
|||
const original = [''];
|
||||
const modified = ['something'];
|
||||
const expected = [
|
||||
createLineChange(1, 1, 1, 1, [
|
||||
createCharChange(0, 0, 0, 0, 0, 0, 0, 0)
|
||||
])
|
||||
createLineChange(1, 1, 1, 1, undefined)
|
||||
];
|
||||
assertDiff(original, modified, expected, true, false, true);
|
||||
});
|
||||
|
@ -538,9 +536,7 @@ suite('Editor Diff - DiffComputer', () => {
|
|||
const original = [''];
|
||||
const modified = ['something', 'something else'];
|
||||
const expected = [
|
||||
createLineChange(1, 1, 1, 2, [
|
||||
createCharChange(0, 0, 0, 0, 0, 0, 0, 0)
|
||||
])
|
||||
createLineChange(1, 1, 1, 2, undefined)
|
||||
];
|
||||
assertDiff(original, modified, expected, true, false, true);
|
||||
});
|
||||
|
@ -549,9 +545,7 @@ suite('Editor Diff - DiffComputer', () => {
|
|||
const original = ['something', 'something else'];
|
||||
const modified = [''];
|
||||
const expected = [
|
||||
createLineChange(1, 2, 1, 1, [
|
||||
createCharChange(0, 0, 0, 0, 0, 0, 0, 0)
|
||||
])
|
||||
createLineChange(1, 2, 1, 1, undefined)
|
||||
];
|
||||
assertDiff(original, modified, expected, true, false, true);
|
||||
});
|
||||
|
@ -560,9 +554,7 @@ suite('Editor Diff - DiffComputer', () => {
|
|||
const original = ['something'];
|
||||
const modified = [''];
|
||||
const expected = [
|
||||
createLineChange(1, 1, 1, 1, [
|
||||
createCharChange(0, 0, 0, 0, 0, 0, 0, 0)
|
||||
])
|
||||
createLineChange(1, 1, 1, 1, undefined)
|
||||
];
|
||||
assertDiff(original, modified, expected, true, false, true);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import assert = require('assert');
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
|
||||
import { lineRangeMappingFromRangeMappings } from 'vs/editor/common/diff/standardLinesDiffComputer';
|
||||
|
||||
suite('standardLinesDiffCompute', () => {
|
||||
test('1', () => {
|
||||
assert.deepStrictEqual(
|
||||
toJson(
|
||||
lineRangeMappingFromRangeMappings([
|
||||
new RangeMapping(r([1, 1, 1, 1]), r([1, 1, 1, 2])),
|
||||
])
|
||||
),
|
||||
(["{[1,2)->[1,2)}"])
|
||||
);
|
||||
});
|
||||
|
||||
test('2', () => {
|
||||
assert.deepStrictEqual(
|
||||
toJson(
|
||||
lineRangeMappingFromRangeMappings([
|
||||
new RangeMapping(r([1, 1, 1, 2]), r([1, 1, 1, 1])),
|
||||
])
|
||||
),
|
||||
(["{[1,2)->[1,2)}"])
|
||||
);
|
||||
});
|
||||
|
||||
test('3', () => {
|
||||
assert.deepStrictEqual(
|
||||
toJson(
|
||||
lineRangeMappingFromRangeMappings([
|
||||
new RangeMapping(r([1, 1, 2, 1]), r([1, 1, 1, 1])),
|
||||
])
|
||||
),
|
||||
(["{[1,2)->[1,1)}"])
|
||||
);
|
||||
});
|
||||
|
||||
test('4', () => {
|
||||
assert.deepStrictEqual(
|
||||
toJson(
|
||||
lineRangeMappingFromRangeMappings([
|
||||
new RangeMapping(r([1, 1, 1, 1]), r([1, 1, 2, 1])),
|
||||
])
|
||||
),
|
||||
(["{[1,1)->[1,2)}"])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function r(values: [startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number]): Range {
|
||||
return new Range(values[0], values[1], values[2], values[3]);
|
||||
}
|
||||
|
||||
function toJson(mappings: LineRangeMapping[]): unknown {
|
||||
return mappings.map(m => m.toString());
|
||||
}
|
|
@ -5,9 +5,10 @@
|
|||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
import { IEditorWorkerService, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorker';
|
||||
import { IDiffComputationResult, IEditorWorkerService, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorker';
|
||||
import { TextEdit, IInplaceReplaceSupportResult } from 'vs/editor/common/languages';
|
||||
import { IChange, IDiffComputationResult } from 'vs/editor/common/diff/diffComputer';
|
||||
import { IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider';
|
||||
import { IChange } from 'vs/editor/common/diff/smartLinesDiffComputer';
|
||||
|
||||
export class TestEditorWorkerService implements IEditorWorkerService {
|
||||
|
||||
|
@ -15,7 +16,7 @@ export class TestEditorWorkerService implements IEditorWorkerService {
|
|||
|
||||
canComputeUnicodeHighlights(uri: URI): boolean { return false; }
|
||||
async computedUnicodeHighlights(uri: URI): Promise<IUnicodeHighlightsResult> { return { ranges: [], hasMore: false, ambiguousCharacterCount: 0, invisibleCharacterCount: 0, nonBasicAsciiCharacterCount: 0 }; }
|
||||
async computeDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean, maxComputationTime: number): Promise<IDiffComputationResult | null> { return null; }
|
||||
async computeDiff(original: URI, modified: URI, options: IDocumentDiffProviderOptions): Promise<IDiffComputationResult | null> { return null; }
|
||||
canComputeDirtyDiff(original: URI, modified: URI): boolean { return false; }
|
||||
async computeDirtyDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean): Promise<IChange[] | null> { return null; }
|
||||
async computeMoreMinimalEdits(resource: URI, edits: TextEdit[] | null | undefined): Promise<TextEdit[] | undefined> { return undefined; }
|
||||
|
|
8
src/vs/monaco.d.ts
vendored
8
src/vs/monaco.d.ts
vendored
|
@ -700,6 +700,10 @@ declare namespace monaco {
|
|||
* Create a new empty range using this range's start position.
|
||||
*/
|
||||
collapseToStart(): Range;
|
||||
/**
|
||||
* Moves the range by the given amount of lines.
|
||||
*/
|
||||
delta(lineCount: number): Range;
|
||||
/**
|
||||
* Create a new empty range using this range's start position.
|
||||
*/
|
||||
|
@ -3505,6 +3509,10 @@ declare namespace monaco.editor {
|
|||
* Control the wrapping of the diff editor.
|
||||
*/
|
||||
diffWordWrap?: 'off' | 'on' | 'inherit';
|
||||
/**
|
||||
* Diff Algorithm
|
||||
*/
|
||||
diffAlgorithm?: 'smart' | 'experimental';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,7 +23,7 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor
|
|||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { ILineChange } from 'vs/editor/common/diff/diffComputer';
|
||||
import { ILineChange } from 'vs/editor/common/diff/smartLinesDiffComputer';
|
||||
import { IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
|
||||
import { IEditorControl } from 'vs/workbench/common/editor';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
|
|
|
@ -19,7 +19,7 @@ import { ISingleEditOperation } from 'vs/editor/common/core/editOperation';
|
|||
import { IPosition } from 'vs/editor/common/core/position';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
import { ISelection, Selection } from 'vs/editor/common/core/selection';
|
||||
import { ILineChange } from 'vs/editor/common/diff/diffComputer';
|
||||
import { ILineChange } from 'vs/editor/common/diff/smartLinesDiffComputer';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import * as languages from 'vs/editor/common/languages';
|
||||
import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes';
|
||||
|
|
|
@ -9,7 +9,7 @@ import { registerDiffEditorContribution } from 'vs/editor/browser/editorExtensio
|
|||
import { IDiffEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor';
|
||||
import { IDiffComputationResult } from 'vs/editor/common/diff/diffComputer';
|
||||
import { IDiffComputationResult } from 'vs/editor/common/diff/smartLinesDiffComputer';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { localize } from 'vs/nls';
|
||||
import { registerAction2 } from 'vs/platform/actions/common/actions';
|
||||
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor';
|
||||
|
@ -33,6 +34,20 @@ Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).registerEdit
|
|||
MergeEditorSerializer
|
||||
);
|
||||
|
||||
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
|
||||
properties: {
|
||||
'mergeEditor.diffAlgorithm': {
|
||||
type: 'string',
|
||||
enum: ['smart', 'experimental'],
|
||||
default: 'smart',
|
||||
markdownEnumDescriptions: [
|
||||
localize('diffAlgorithm.smart', "Uses the default diffing algorithm."),
|
||||
localize('diffAlgorithm.experimental', "Uses an experimental diffing algorithm."),
|
||||
]
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(OpenResultResource);
|
||||
registerAction2(SetMixedLayout);
|
||||
registerAction2(SetColumnLayout);
|
||||
|
|
|
@ -16,11 +16,12 @@ import { ILabelService } from 'vs/platform/label/common/label';
|
|||
import { DEFAULT_EDITOR_ASSOCIATION, EditorInputCapabilities, IEditorIdentifier, IResourceMergeEditorInput, isResourceMergeEditorInput, IUntypedEditorInput } from 'vs/workbench/common/editor';
|
||||
import { EditorInput, IEditorCloseHandler } from 'vs/workbench/common/editor/editorInput';
|
||||
import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput';
|
||||
import { EditorWorkerServiceDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer';
|
||||
import { MergeDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer';
|
||||
import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { ILanguageSupport, ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { autorun } from 'vs/base/common/observable';
|
||||
import { WorkerBasedDocumentDiffProvider } from 'vs/editor/browser/widget/workerBasedDocumentDiffProvider';
|
||||
|
||||
export class MergeEditorInputData {
|
||||
constructor(
|
||||
|
@ -117,7 +118,7 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements
|
|||
this.input2.detail,
|
||||
this.input2.description,
|
||||
result.object.textEditorModel,
|
||||
this._instaService.createInstance(EditorWorkerServiceDiffComputer),
|
||||
this._instaService.createInstance(MergeDiffComputer, this._instaService.createInstance(WorkerBasedDocumentDiffProvider)),
|
||||
{
|
||||
resetUnknownOnInitialization: true
|
||||
},
|
||||
|
|
|
@ -3,95 +3,71 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IReader, observableFromEvent } from 'vs/base/common/observable';
|
||||
import { isDefined } from 'vs/base/common/types';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ICharChange, IDiffComputationResult, ILineChange } from 'vs/editor/common/diff/diffComputer';
|
||||
import { IDocumentDiffProvider } from 'vs/editor/common/diff/documentDiffProvider';
|
||||
import { LineRange as DiffLineRange, RangeMapping as DiffRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange';
|
||||
import { DetailedLineRangeMapping, RangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping';
|
||||
|
||||
export interface IDiffComputer {
|
||||
computeDiff(textModel1: ITextModel, textModel2: ITextModel): Promise<IDiffComputerResult>;
|
||||
export interface IMergeDiffComputer {
|
||||
computeDiff(textModel1: ITextModel, textModel2: ITextModel, reader: IReader): Promise<IMergeDiffComputerResult>;
|
||||
}
|
||||
|
||||
export interface IDiffComputerResult {
|
||||
export interface IMergeDiffComputerResult {
|
||||
diffs: DetailedLineRangeMapping[] | null;
|
||||
}
|
||||
|
||||
export class EditorWorkerServiceDiffComputer implements IDiffComputer {
|
||||
constructor(@IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService) { }
|
||||
export class MergeDiffComputer implements IMergeDiffComputer {
|
||||
|
||||
async computeDiff(textModel1: ITextModel, textModel2: ITextModel): Promise<IDiffComputerResult> {
|
||||
const diffs = await this.editorWorkerService.computeDiff(textModel1.uri, textModel2.uri, false, 1000);
|
||||
if (textModel1.isDisposed() || textModel2.isDisposed()) {
|
||||
// In the meantime, models could be disposed -> early return
|
||||
return { diffs: null };
|
||||
}
|
||||
if (!diffs) {
|
||||
return { diffs: null };
|
||||
}
|
||||
return { diffs: EditorWorkerServiceDiffComputer.fromDiffComputationResult(diffs, textModel1, textModel2) };
|
||||
}
|
||||
|
||||
public static fromDiffComputationResult(result: IDiffComputationResult, textModel1: ITextModel, textModel2: ITextModel): DetailedLineRangeMapping[] {
|
||||
return result.changes.map((c) => fromLineChange(c, textModel1, textModel2));
|
||||
}
|
||||
}
|
||||
|
||||
function fromLineChange(lineChange: ILineChange, originalTextModel: ITextModel, modifiedTextModel: ITextModel): DetailedLineRangeMapping {
|
||||
let originalRange: LineRange;
|
||||
if (lineChange.originalEndLineNumber === 0) {
|
||||
// Insertion
|
||||
originalRange = new LineRange(lineChange.originalStartLineNumber + 1, 0);
|
||||
} else {
|
||||
originalRange = new LineRange(lineChange.originalStartLineNumber, lineChange.originalEndLineNumber - lineChange.originalStartLineNumber + 1);
|
||||
}
|
||||
|
||||
let modifiedRange: LineRange;
|
||||
if (lineChange.modifiedEndLineNumber === 0) {
|
||||
// Deletion
|
||||
modifiedRange = new LineRange(lineChange.modifiedStartLineNumber + 1, 0);
|
||||
} else {
|
||||
modifiedRange = new LineRange(lineChange.modifiedStartLineNumber, lineChange.modifiedEndLineNumber - lineChange.modifiedStartLineNumber + 1);
|
||||
}
|
||||
|
||||
let innerDiffs = lineChange.charChanges?.map(c => rangeMappingFromCharChange(c, originalTextModel, modifiedTextModel)).filter(isDefined);
|
||||
if (!innerDiffs || innerDiffs.length === 0) {
|
||||
innerDiffs = [rangeMappingFromLineRanges(originalRange, modifiedRange)];
|
||||
}
|
||||
|
||||
return new DetailedLineRangeMapping(
|
||||
originalRange,
|
||||
originalTextModel,
|
||||
modifiedRange,
|
||||
modifiedTextModel,
|
||||
innerDiffs
|
||||
private readonly mergeAlgorithm = observableFromEvent(
|
||||
this.configurationService.onDidChangeConfiguration,
|
||||
() => /** @description config: mergeAlgorithm.diffAlgorithm */ this.configurationService.getValue<'smart' | 'experimental'>('mergeEditor.diffAlgorithm')
|
||||
);
|
||||
|
||||
constructor(
|
||||
private readonly documentDiffProvider: IDocumentDiffProvider,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
) {
|
||||
}
|
||||
|
||||
async computeDiff(textModel1: ITextModel, textModel2: ITextModel, reader: IReader): Promise<IMergeDiffComputerResult> {
|
||||
const diffAlgorithm = this.mergeAlgorithm.read(reader);
|
||||
const result = await this.documentDiffProvider.computeDiff(
|
||||
textModel1,
|
||||
textModel2,
|
||||
{
|
||||
ignoreTrimWhitespace: false,
|
||||
maxComputationTime: 0,
|
||||
diffAlgorithm,
|
||||
}
|
||||
);
|
||||
|
||||
const changes = result.changes.map(c =>
|
||||
new DetailedLineRangeMapping(
|
||||
toLineRange(c.originalRange),
|
||||
textModel1,
|
||||
toLineRange(c.modifiedRange),
|
||||
textModel2,
|
||||
c.innerChanges?.map(ic => normalizeRangeMapping(toRangeMapping(ic), textModel1, textModel2)).filter(isDefined)
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
diffs: changes
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function rangeMappingFromLineRanges(originalRange: LineRange, modifiedRange: LineRange): RangeMapping {
|
||||
return new RangeMapping(
|
||||
new Range(
|
||||
originalRange.startLineNumber,
|
||||
1,
|
||||
originalRange.endLineNumberExclusive,
|
||||
1,
|
||||
),
|
||||
new Range(
|
||||
modifiedRange.startLineNumber,
|
||||
1,
|
||||
modifiedRange.endLineNumberExclusive,
|
||||
1,
|
||||
)
|
||||
);
|
||||
function toLineRange(range: DiffLineRange): LineRange {
|
||||
return new LineRange(range.startLineNumber, range.length);
|
||||
}
|
||||
|
||||
function rangeMappingFromCharChange(charChange: ICharChange, inputTextModel: ITextModel, modifiedTextModel: ITextModel): RangeMapping | undefined {
|
||||
return normalizeRangeMapping(new RangeMapping(
|
||||
new Range(charChange.originalStartLineNumber, charChange.originalStartColumn, charChange.originalEndLineNumber, charChange.originalEndColumn),
|
||||
new Range(charChange.modifiedStartLineNumber, charChange.modifiedStartColumn, charChange.modifiedEndLineNumber, charChange.modifiedEndColumn)
|
||||
), inputTextModel, modifiedTextModel);
|
||||
function toRangeMapping(mapping: DiffRangeMapping): RangeMapping {
|
||||
return new RangeMapping(mapping.originalRange, mapping.modifiedRange);
|
||||
}
|
||||
|
||||
function normalizeRangeMapping(rangeMapping: RangeMapping, inputTextModel: ITextModel, outputTextModel: ITextModel): RangeMapping | undefined {
|
||||
|
@ -102,6 +78,11 @@ function normalizeRangeMapping(rangeMapping: RangeMapping, inputTextModel: IText
|
|||
return undefined;
|
||||
}
|
||||
|
||||
if (rangeMapping.inputRange.startLineNumber > inputTextModel.getLineCount()
|
||||
|| rangeMapping.outputRange.startLineNumber > outputTextModel.getLineCount()) {
|
||||
return rangeMapping;
|
||||
}
|
||||
|
||||
const originalStartsAtEndOfLine = isAtEndOfLine(rangeMapping.inputRange.startLineNumber, rangeMapping.inputRange.startColumn, inputTextModel);
|
||||
const modifiedStartsAtEndOfLine = isAtEndOfLine(rangeMapping.outputRange.startLineNumber, rangeMapping.outputRange.startColumn, outputTextModel);
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import { ILanguageService } from 'vs/editor/common/languages/language';
|
|||
import { ITextModel, ITextSnapshot } from 'vs/editor/common/model';
|
||||
import { IModelService } from 'vs/editor/common/services/model';
|
||||
import { EditorModel } from 'vs/workbench/common/editor/editorModel';
|
||||
import { IDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer';
|
||||
import { IMergeDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer';
|
||||
import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange';
|
||||
import { DetailedLineRangeMapping, DocumentMapping, LineRangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping';
|
||||
import { TextModelDiffChangeReason, TextModelDiffs, TextModelDiffState } from 'vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs';
|
||||
|
@ -133,7 +133,7 @@ export class MergeEditorModel extends EditorModel {
|
|||
readonly input2Detail: string | undefined,
|
||||
readonly input2Description: string | undefined,
|
||||
readonly result: ITextModel,
|
||||
private readonly diffComputer: IDiffComputer,
|
||||
private readonly diffComputer: IMergeDiffComputer,
|
||||
options: { resetUnknownOnInitialization: boolean },
|
||||
@IModelService private readonly modelService: IModelService,
|
||||
@ILanguageService private readonly languageService: ILanguageService,
|
||||
|
|
|
@ -11,8 +11,8 @@ import { DetailedLineRangeMapping } from 'vs/workbench/contrib/mergeEditor/brows
|
|||
import { LineRangeEdit } from 'vs/workbench/contrib/mergeEditor/browser/model/editing';
|
||||
import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange';
|
||||
import { ReentrancyBarrier } from 'vs/workbench/contrib/mergeEditor/browser/utils';
|
||||
import { IDiffComputer } from './diffComputer';
|
||||
import { IObservable, IReader, ITransaction, observableValue, transaction } from 'vs/base/common/observable';
|
||||
import { IMergeDiffComputer } from './diffComputer';
|
||||
import { autorun, IObservable, IReader, ITransaction, observableValue, transaction } from 'vs/base/common/observable';
|
||||
|
||||
export class TextModelDiffs extends Disposable {
|
||||
private updateCount = 0;
|
||||
|
@ -25,13 +25,19 @@ export class TextModelDiffs extends Disposable {
|
|||
constructor(
|
||||
private readonly baseTextModel: ITextModel,
|
||||
private readonly textModel: ITextModel,
|
||||
private readonly diffComputer: IDiffComputer,
|
||||
private readonly diffComputer: IMergeDiffComputer,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.update(true);
|
||||
this._register(baseTextModel.onDidChangeContent(this.barrier.makeExclusive(() => this.update())));
|
||||
this._register(textModel.onDidChangeContent(this.barrier.makeExclusive(() => this.update())));
|
||||
const counter = observableValue('invalidation counter', 0);
|
||||
|
||||
this._register(autorun('Update diff state', reader => {
|
||||
counter.read(reader);
|
||||
this.update(reader);
|
||||
}));
|
||||
|
||||
this._register(baseTextModel.onDidChangeContent(this.barrier.makeExclusive(() => { counter.set(counter.get() + 1, undefined); })));
|
||||
this._register(textModel.onDidChangeContent(this.barrier.makeExclusive(() => { counter.set(counter.get() + 1, undefined); })));
|
||||
this._register(toDisposable(() => {
|
||||
this.isDisposed = true;
|
||||
}));
|
||||
|
@ -45,41 +51,47 @@ export class TextModelDiffs extends Disposable {
|
|||
return this._diffs;
|
||||
}
|
||||
|
||||
private async update(initializing: boolean = false): Promise<void> {
|
||||
private isInitializing = true;
|
||||
|
||||
private update(reader: IReader): void {
|
||||
this.updateCount++;
|
||||
const currentUpdateCount = this.updateCount;
|
||||
|
||||
if (this._state.get() === TextModelDiffState.initializing) {
|
||||
initializing = true;
|
||||
this.isInitializing = true;
|
||||
}
|
||||
|
||||
transaction(tx => {
|
||||
/** @description Starting Diff Computation. */
|
||||
this._state.set(
|
||||
initializing ? TextModelDiffState.initializing : TextModelDiffState.updating,
|
||||
this.isInitializing ? TextModelDiffState.initializing : TextModelDiffState.updating,
|
||||
tx,
|
||||
TextModelDiffChangeReason.other
|
||||
);
|
||||
});
|
||||
|
||||
const result = await this.diffComputer.computeDiff(this.baseTextModel, this.textModel);
|
||||
if (this.isDisposed) {
|
||||
return;
|
||||
}
|
||||
const result = this.diffComputer.computeDiff(this.baseTextModel, this.textModel, reader);
|
||||
|
||||
if (currentUpdateCount !== this.updateCount) {
|
||||
// There is a newer update call
|
||||
return;
|
||||
}
|
||||
|
||||
transaction(tx => {
|
||||
/** @description Completed Diff Computation */
|
||||
if (result.diffs) {
|
||||
this._state.set(TextModelDiffState.upToDate, tx, TextModelDiffChangeReason.textChange);
|
||||
this._diffs.set(result.diffs, tx, TextModelDiffChangeReason.textChange);
|
||||
} else {
|
||||
this._state.set(TextModelDiffState.error, tx, TextModelDiffChangeReason.textChange);
|
||||
result.then((result) => {
|
||||
if (this.isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentUpdateCount !== this.updateCount) {
|
||||
// There is a newer update call
|
||||
return;
|
||||
}
|
||||
|
||||
transaction(tx => {
|
||||
/** @description Completed Diff Computation */
|
||||
if (result.diffs) {
|
||||
this._state.set(TextModelDiffState.upToDate, tx, TextModelDiffChangeReason.textChange);
|
||||
this._diffs.set(result.diffs, tx, TextModelDiffChangeReason.textChange);
|
||||
} else {
|
||||
this._state.set(TextModelDiffState.error, tx, TextModelDiffChangeReason.textChange);
|
||||
}
|
||||
this.isInitializing = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -8,11 +8,11 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
|||
import { transaction } from 'vs/base/common/observable';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { linesDiffComputers } from 'vs/editor/common/diff/linesDiffComputers';
|
||||
import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model';
|
||||
import { EditorSimpleWorker } from 'vs/editor/common/services/editorSimpleWorker';
|
||||
import { createModelServices, createTextModel } from 'vs/editor/test/common/testTextModel';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { EditorWorkerServiceDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer';
|
||||
import { MergeDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer';
|
||||
import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel';
|
||||
|
||||
suite('merge editor model', () => {
|
||||
|
@ -268,21 +268,22 @@ class MergeModelInterface extends Disposable {
|
|||
'',
|
||||
'',
|
||||
resultTextModel,
|
||||
{
|
||||
async computeDiff(textModel1, textModel2) {
|
||||
const result = EditorSimpleWorker.computeDiff(textModel1, textModel2, false, 10000);
|
||||
if (!result) {
|
||||
return { diffs: null };
|
||||
}
|
||||
return {
|
||||
diffs: EditorWorkerServiceDiffComputer.fromDiffComputationResult(
|
||||
result,
|
||||
textModel1,
|
||||
textModel2
|
||||
),
|
||||
};
|
||||
},
|
||||
}, {
|
||||
instantiationService.createInstance(MergeDiffComputer,
|
||||
{
|
||||
// Don't go through the webworker to improve unit test performance & reduce dependencies
|
||||
async computeDiff(textModel1, textModel2) {
|
||||
const result = linesDiffComputers.smart.computeDiff(
|
||||
textModel1.getLinesContent(),
|
||||
textModel2.getLinesContent(),
|
||||
{ ignoreTrimWhitespace: false, maxComputationTime: 10000 }
|
||||
);
|
||||
return {
|
||||
changes: result.changes,
|
||||
quitEarly: result.quitEarly,
|
||||
identical: result.changes.length === 0
|
||||
};
|
||||
},
|
||||
}), {
|
||||
resetUnknownOnInitialization: false
|
||||
}
|
||||
));
|
||||
|
|
|
@ -94,7 +94,8 @@ export const fixedDiffEditorOptions: IDiffEditorConstructionOptions = {
|
|||
renderIndicators: true,
|
||||
readOnly: false,
|
||||
isInEmbeddedEditor: true,
|
||||
renderOverviewRuler: false
|
||||
renderOverviewRuler: false,
|
||||
diffAlgorithm: 'smart',
|
||||
};
|
||||
|
||||
class PropertyHeader extends Disposable {
|
||||
|
|
|
@ -15,7 +15,7 @@ import { basename } from 'vs/base/common/path';
|
|||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { ISplice } from 'vs/base/common/sequence';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { ILineChange } from 'vs/editor/common/diff/diffComputer';
|
||||
import { ILineChange } from 'vs/editor/common/diff/smartLinesDiffComputer';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { Command, WorkspaceEditMetadata } from 'vs/editor/common/languages';
|
||||
import { IReadonlyTextBuffer } from 'vs/editor/common/model';
|
||||
|
|
|
@ -50,7 +50,7 @@ import { Codicon } from 'vs/base/common/codicons';
|
|||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { TextCompareEditorActiveContext } from 'vs/workbench/common/contextkeys';
|
||||
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
|
||||
import { IChange } from 'vs/editor/common/diff/diffComputer';
|
||||
import { IChange } from 'vs/editor/common/diff/smartLinesDiffComputer';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { editorGutter } from 'vs/editor/common/core/editorColorRegistry';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
|
@ -297,7 +297,8 @@ class DirtyDiffWidget extends PeekViewWidget {
|
|||
minimap: { enabled: false },
|
||||
renderSideBySide: false,
|
||||
readOnly: false,
|
||||
renderIndicators: false
|
||||
renderIndicators: false,
|
||||
diffAlgorithm: 'smart',
|
||||
};
|
||||
|
||||
this.diffEditor = this.instantiationService.createInstance(EmbeddedDiffEditorWidget, container, options, this.editor);
|
||||
|
|
|
@ -836,6 +836,7 @@ const diffEditorOptions: IDiffEditorConstructionOptions = {
|
|||
renderSideBySide: true,
|
||||
originalAriaLabel: localize('testingOutputExpected', 'Expected result'),
|
||||
modifiedAriaLabel: localize('testingOutputActual', 'Actual result'),
|
||||
diffAlgorithm: 'smart',
|
||||
};
|
||||
|
||||
const isDiffable = (message: ITestMessage): message is ITestErrorMessage & { actualOutput: string; expectedOutput: string } =>
|
||||
|
|
Loading…
Reference in a new issue