Allows to pass in a custom diffing algorithm when instantiating the monaco diff editor. (#165179)

* Allows to pass in a custom diffing algorithm when instantiating the monaco diff editor.

* Undo launch.json change
This commit is contained in:
Henning Dieterichs 2022-11-16 08:48:39 +01:00 committed by GitHub
parent 8bd052125b
commit 3e19ba91ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 431 additions and 212 deletions

View file

@ -73,6 +73,8 @@ export interface ICommandHandler {
#include(vs/editor/common/core/wordHelper): IWordAtPosition
#includeAll(vs/editor/common/model): IScrollEvent
#include(vs/editor/common/diff/smartLinesDiffComputer): IChange, ICharChange, ILineChange
#include(vs/editor/common/diff/documentDiffProvider): IDocumentDiffProvider, IDocumentDiffProviderOptions, IDocumentDiff
#include(vs/editor/common/diff/linesDiffComputer): LineRangeMapping, LineRange, RangeMapping
#include(vs/editor/common/core/dimension): IDimension
#includeAll(vs/editor/common/editorCommon): IScrollEvent
#includeAll(vs/editor/common/textModelEvents):

View file

@ -14,7 +14,7 @@ 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 { IDiffComputationResult, IEditorWorkerService, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorker';
import { DiffAlgorithmName, 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,7 +26,8 @@ import { UnicodeHighlighterOptions } from 'vs/editor/common/services/unicodeText
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';
import { IDocumentDiff, IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider';
import { LineRangeMapping, LineRange, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
/**
* Stop syncing a model to the worker if it was not needed for 1 min.
@ -95,8 +96,31 @@ export class EditorWorkerService extends Disposable implements IEditorWorkerServ
return this._workerManager.withWorker().then(client => client.computedUnicodeHighlights(uri, options, range));
}
public computeDiff(original: URI, modified: URI, options: IDocumentDiffProviderOptions): Promise<IDiffComputationResult | null> {
return this._workerManager.withWorker().then(client => client.computeDiff(original, modified, options));
public async computeDiff(original: URI, modified: URI, options: IDocumentDiffProviderOptions, algorithm: DiffAlgorithmName): Promise<IDocumentDiff | null> {
const result = await this._workerManager.withWorker().then(client => client.computeDiff(original, modified, options, algorithm));
if (!result) {
return null;
}
// Convert from space efficient JSON data to rich objects.
const diff: IDocumentDiff = {
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])
)
)
)
),
};
return diff;
}
public canComputeDirtyDiff(original: URI, modified: URI): boolean {
@ -492,9 +516,9 @@ export class EditorWorkerClient extends Disposable implements IEditorWorkerClien
});
}
public computeDiff(original: URI, modified: URI, options: IDocumentDiffProviderOptions): Promise<IDiffComputationResult | null> {
public computeDiff(original: URI, modified: URI, options: IDocumentDiffProviderOptions, algorithm: DiffAlgorithmName): Promise<IDiffComputationResult | null> {
return this._withSyncedResources([original, modified], /* forceLargeModels */true).then(proxy => {
return proxy.computeDiff(original.toString(), modified.toString(), options);
return proxy.computeDiff(original.toString(), modified.toString(), options, algorithm);
});
}

View file

@ -3,61 +3,60 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/diffEditor';
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
import { createFastDomNode, FastDomNode } from 'vs/base/browser/fastDomNode';
import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor';
import { ISashEvent, IVerticalSashLayoutProvider, Orientation, Sash, SashState } from 'vs/base/browser/ui/sash/sash';
import * as assert from 'vs/base/common/assert';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { ISashEvent, IVerticalSashLayoutProvider, Sash, SashState, Orientation } from 'vs/base/browser/ui/sash/sash';
import { RunOnceScheduler } from 'vs/base/common/async';
import { Codicon } from 'vs/base/common/codicons';
import { Color } from 'vs/base/common/color';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { Constants } from 'vs/base/common/uint';
import { URI } from 'vs/base/common/uri';
import 'vs/css!./media/diffEditor';
import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo';
import { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll';
import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration';
import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver';
import * as editorBrowser from 'vs/editor/browser/editorBrowser';
import { EditorExtensionsRegistry, IDiffEditorContributionDescription } from 'vs/editor/browser/editorExtensions';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll';
import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget';
import { DiffReview } from 'vs/editor/browser/widget/diffReview';
import { IDiffEditorOptions, EditorLayoutInfo, EditorOption, EditorOptions, EditorFontLigatures, stringSet as validateStringSetOption, boolean as validateBooleanOption, ValidDiffEditorBaseOptions, clampedInt } from 'vs/editor/common/config/editorOptions';
import { IDiffLinesChange, InlineDiffMargin } from 'vs/editor/browser/widget/inlineDiffMargin';
import { WorkerBasedDocumentDiffProvider } from 'vs/editor/browser/widget/workerBasedDocumentDiffProvider';
import { boolean as validateBooleanOption, clampedInt, EditorFontLigatures, EditorLayoutInfo, EditorOption, EditorOptions, IDiffEditorOptions, stringSet as validateStringSetOption, ValidDiffEditorBaseOptions } from 'vs/editor/common/config/editorOptions';
import { FontInfo } from 'vs/editor/common/config/fontInfo';
import { IDimension } from 'vs/editor/common/core/dimension';
import { IPosition, Position } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { ISelection, Selection } from 'vs/editor/common/core/selection';
import { StringBuilder } from 'vs/editor/common/core/stringBuilder';
import { IChange, ICharChange, IDiffComputationResult, ILineChange } from 'vs/editor/common/diff/smartLinesDiffComputer';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { OverviewRulerZone } from 'vs/editor/common/viewModel/overviewZoneManager';
import { ILineBreaksComputer } from 'vs/editor/common/modelLineProjectionData';
import { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens';
import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations';
import { RenderLineInput, renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer';
import { IEditorWhitespace, InlineDecoration, InlineDecorationType, IViewModel, ViewLineRenderingData } from 'vs/editor/common/viewModel';
import { OverviewRulerZone } from 'vs/editor/common/viewModel/overviewZoneManager';
import * as nls from 'vs/nls';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { defaultInsertColor, defaultRemoveColor, diffBorder, diffInserted, diffInsertedOutline, diffRemoved, diffRemovedOutline, scrollbarShadow, scrollbarSliderBackground, scrollbarSliderHoverBackground, scrollbarSliderActiveBackground, diffDiagonalFill, diffInsertedLineGutter, diffRemovedLineGutter, diffInsertedLine, diffRemovedLine, diffOverviewRulerInserted, diffOverviewRulerRemoved } from 'vs/platform/theme/common/colorRegistry';
import { IColorTheme, IThemeService, getThemeTypeSelector, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IDiffLinesChange, InlineDiffMargin } from 'vs/editor/browser/widget/inlineDiffMargin';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { Constants } from 'vs/base/common/uint';
import { EditorExtensionsRegistry, IDiffEditorContributionDescription } from 'vs/editor/browser/editorExtensions';
import { onUnexpectedError } from 'vs/base/common/errors';
import { IEditorProgressService, IProgressRunner } from 'vs/platform/progress/common/progress';
import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver';
import { Codicon } from 'vs/base/common/codicons';
import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor';
import { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens';
import { FontInfo } from 'vs/editor/common/config/fontInfo';
import { defaultInsertColor, defaultRemoveColor, diffBorder, diffDiagonalFill, diffInserted, diffInsertedLine, diffInsertedLineGutter, diffInsertedOutline, diffOverviewRulerInserted, diffOverviewRulerRemoved, diffRemoved, diffRemovedLine, diffRemovedLineGutter, diffRemovedOutline, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground } from 'vs/platform/theme/common/colorRegistry';
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/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';
import { getThemeTypeSelector, IColorTheme, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService';
export interface IDiffCodeEditorWidgetOptions {
originalEditor?: ICodeEditorWidgetOptions;
@ -227,7 +226,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE
private readonly _updateDecorationsRunner: RunOnceScheduler;
private readonly _documentDiffProvider: IDocumentDiffProvider;
private readonly _documentDiffProvider: WorkerBasedDocumentDiffProvider;
private readonly _contextKeyService: IContextKeyService;
private readonly _instantiationService: IInstantiationService;
private readonly _codeEditorService: ICodeEditorService;
@ -251,7 +250,9 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE
) {
super();
this._documentDiffProvider = instantiationService.createInstance(WorkerBasedDocumentDiffProvider);
this._documentDiffProvider = this._register(instantiationService.createInstance(WorkerBasedDocumentDiffProvider, options));
this._register(this._documentDiffProvider.onDidChange(e => this._beginUpdateDecorationsSoon()));
this._codeEditorService = codeEditorService;
this._contextKeyService = this._register(contextKeyService.createScoped(domElement));
this._instantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, this._contextKeyService]));
@ -762,7 +763,8 @@ 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 || changed.diffAlgorithm));
const beginUpdateDecorationsSoon = (this._isVisible && (changed.maxComputationTime || changed.maxFileSize));
this._documentDiffProvider.setOptions(newOptions);
if (beginUpdateDecorations) {
this._beginUpdateDecorations();
@ -1092,7 +1094,11 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE
}
private _beginUpdateDecorations(): void {
this._beginUpdateDecorationsTimeout = -1;
if (this._beginUpdateDecorationsTimeout !== -1) {
// Cancel any pending requests in case this method is called directly
window.clearTimeout(this._beginUpdateDecorationsTimeout);
this._beginUpdateDecorationsTimeout = -1;
}
const currentOriginalModel = this._originalEditor.getModel();
const currentModifiedModel = this._modifiedEditor.getModel();
if (!currentOriginalModel || !currentModifiedModel) {
@ -1126,8 +1132,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE
this._setState(editorBrowser.DiffEditorState.ComputingDiff);
this._documentDiffProvider.computeDiff(currentOriginalModel, currentModifiedModel, {
ignoreTrimWhitespace: this._options.ignoreTrimWhitespace,
maxComputationTime: this._options.maxComputationTime,
diffAlgorithm: this._options.diffAlgorithm,
maxComputationTimeMs: this._options.maxComputationTime,
}).then(result => {
if (currentToken === this._diffComputationToken
&& currentOriginalModel === this._originalEditor.getModel()

View file

@ -3,43 +3,63 @@
* 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 { Emitter, Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
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';
import { DiffAlgorithmName, IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
export class WorkerBasedDocumentDiffProvider implements IDocumentDiffProvider, IDisposable {
private onDidChangeEventEmitter = new Emitter<void>();
public readonly onDidChange: Event<void> = this.onDidChangeEventEmitter.event;
private diffAlgorithm: DiffAlgorithmName | IDocumentDiffProvider = 'smart';
private diffAlgorithmOnDidChangeSubscription: IDisposable | undefined = undefined;
export class WorkerBasedDocumentDiffProvider implements IDocumentDiffProvider {
constructor(
options: IWorkerBasedDocumentDiffProviderOptions,
@IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService,
) {
this.setOptions(options);
}
public dispose(): void {
this.diffAlgorithmOnDidChangeSubscription?.dispose();
}
async computeDiff(original: ITextModel, modified: ITextModel, options: IDocumentDiffProviderOptions): Promise<IDocumentDiff> {
const result = await this.editorWorkerService.computeDiff(original.uri, modified.uri, options);
if (typeof this.diffAlgorithm !== 'string') {
return this.diffAlgorithm.computeDiff(original, modified, options);
}
const result = await this.editorWorkerService.computeDiff(original.uri, modified.uri, options, this.diffAlgorithm);
if (!result) {
throw new Error('no diff result available');
}
// Convert from space efficient JSON data to rich objects.
const diff: IDocumentDiff = {
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])
)
)
)
),
};
return diff;
return result;
}
public setOptions(newOptions: IWorkerBasedDocumentDiffProviderOptions): void {
let didChange = false;
if (newOptions.diffAlgorithm) {
if (this.diffAlgorithm !== newOptions.diffAlgorithm) {
this.diffAlgorithmOnDidChangeSubscription?.dispose();
this.diffAlgorithmOnDidChangeSubscription = undefined;
this.diffAlgorithm = newOptions.diffAlgorithm;
if (typeof newOptions.diffAlgorithm !== 'string') {
this.diffAlgorithmOnDidChangeSubscription = newOptions.diffAlgorithm.onDidChange(() => this.onDidChangeEventEmitter.fire());
}
didChange = true;
}
}
if (didChange) {
this.onDidChangeEventEmitter.fire();
}
}
}
interface IWorkerBasedDocumentDiffProviderOptions {
readonly diffAlgorithm?: 'smart' | 'experimental' | IDocumentDiffProvider;
}

View file

@ -15,6 +15,7 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as arrays from 'vs/base/common/arrays';
import * as objects from 'vs/base/common/objects';
import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/core/textModelDefaults';
import { IDocumentDiffProvider } from 'vs/editor/common/diff/documentDiffProvider';
//#region typed options
@ -739,7 +740,7 @@ export interface IDiffEditorBaseOptions {
/**
* Diff Algorithm
*/
diffAlgorithm?: 'smart' | 'experimental';
diffAlgorithm?: 'smart' | 'experimental' | IDocumentDiffProvider;
}
/**

View file

@ -3,21 +3,57 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { ITextModel } from 'vs/editor/common/model';
/**
* A document diff provider computes the diff between two text models.
*/
export interface IDocumentDiffProvider {
/**
* Computes the diff between the text models `original` and `modified`.
*/
computeDiff(original: ITextModel, modified: ITextModel, options: IDocumentDiffProviderOptions): Promise<IDocumentDiff>;
/**
* Is fired when settings of the diff algorithm change that could alter the result of the diffing computation.
* Any user of this provider should recompute the diff when this event is fired.
*/
onDidChange: Event<void>;
}
/**
* Options for the diff computation.
*/
export interface IDocumentDiffProviderOptions {
/**
* When set to true, the diff should ignore whitespace changes.i
*/
ignoreTrimWhitespace: boolean;
maxComputationTime: number;
diffAlgorithm: 'smart' | 'experimental';
/**
* A diff computation should throw if it takes longer than this value.
*/
maxComputationTimeMs: number;
}
/**
* Represents a diff between two text models.
*/
export interface IDocumentDiff {
/**
* If true, both text models are identical (byte-wise).
*/
readonly identical: boolean;
/**
* If true, the diff computation timed out and the diff might not be accurate.
*/
readonly quitEarly: boolean;
/**
* Maps all modified line ranges in the original to the corresponding line ranges in the modified text model.
*/
readonly changes: LineRangeMapping[];
}

View file

@ -10,8 +10,8 @@ export interface ILinesDiffComputer {
}
export interface ILinesDiffComputerOptions {
ignoreTrimWhitespace: boolean;
maxComputationTime: number;
readonly ignoreTrimWhitespace: boolean;
readonly maxComputationTimeMs: number;
}
export interface ILinesDiff {
@ -19,58 +19,125 @@ export interface ILinesDiff {
readonly changes: LineRangeMapping[];
}
/**
* Maps a line range in the original text model to a line range in the modified text model.
*/
export class LineRangeMapping {
constructor(
readonly originalRange: LineRange,
readonly modifiedRange: LineRange,
/**
* Meaning of `undefined` unclear.
*/
readonly innerChanges: RangeMapping[] | undefined,
) { }
/**
* The line range in the original text model.
*/
public readonly originalRange: LineRange;
toString(): string {
return `{${this.originalRange.toString()}->${this.modifiedRange.toString()}}`;
/**
* The line range in the modified text model.
*/
public readonly modifiedRange: LineRange;
/**
* If inner changes have not been computed, this is set to undefined.
* Otherwise, it represents the character-level diff in this line range.
* The original range of each range mapping should be contained in the original line range (same for modified).
* Must not be an empty array.
*/
public readonly innerChanges: RangeMapping[] | undefined;
constructor(
originalRange: LineRange,
modifiedRange: LineRange,
innerChanges: RangeMapping[] | undefined,
) {
this.originalRange = originalRange;
this.modifiedRange = modifiedRange;
this.innerChanges = innerChanges;
}
}
export class RangeMapping {
constructor(
readonly originalRange: Range,
readonly modifiedRange: Range,
) { }
toString(): string {
public toString(): string {
return `{${this.originalRange.toString()}->${this.modifiedRange.toString()}}`;
}
}
/**
* 1-based.
*/
export class LineRange {
constructor(public readonly startLineNumber: number, public readonly endLineNumberExclusive: number) { }
* Maps a range in the original text model to a range in the modified text model.
*/
export class RangeMapping {
/**
* The original range.
*/
readonly originalRange: Range;
/**
* The modified range.
*/
readonly modifiedRange: Range;
constructor(
originalRange: Range,
modifiedRange: Range,
) {
this.originalRange = originalRange;
this.modifiedRange = modifiedRange;
}
public toString(): string {
return `{${this.originalRange.toString()}->${this.modifiedRange.toString()}}`;
}
}
/**
* A range of lines (1-based).
*/
export class LineRange {
/**
* The start line number.
*/
public readonly startLineNumber: number;
/**
* The end line number (exclusive).
*/
public readonly endLineNumberExclusive: number;
constructor(
startLineNumber: number,
endLineNumberExclusive: number,
) {
this.startLineNumber = startLineNumber;
this.endLineNumberExclusive = endLineNumberExclusive;
}
/**
* Indicates if this line range is empty.
*/
get isEmpty(): boolean {
return this.startLineNumber === this.endLineNumberExclusive;
}
/**
* Moves this line range by the given offset of line numbers.
*/
public delta(offset: number): LineRange {
return new LineRange(this.startLineNumber + offset, this.endLineNumberExclusive + offset);
}
/**
* The number of lines this line range spans.
*/
public get length(): number {
return this.endLineNumberExclusive - this.startLineNumber;
}
toString(): string {
return `[${this.startLineNumber},${this.endLineNumberExclusive})`;
}
/**
* Creates a line range that combines this and the given line range.
*/
public join(other: LineRange): LineRange {
return new LineRange(
Math.min(this.startLineNumber, other.startLineNumber),
Math.max(this.endLineNumberExclusive, other.endLineNumberExclusive)
);
}
public toString(): string {
return `[${this.startLineNumber},${this.endLineNumberExclusive})`;
}
}

View file

@ -15,7 +15,7 @@ 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,
maxComputationTime: options.maxComputationTimeMs,
shouldIgnoreTrimWhitespace: options.ignoreTrimWhitespace,
shouldComputeCharChanges: true,
shouldMakePrettyDiff: true,

View file

@ -16,7 +16,7 @@ import { ensureValidWordDefinition, getWordAtText, IWordAtPosition } from 'vs/ed
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 { IDiffComputationResult, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorker';
import { DiffAlgorithmName, 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';
@ -384,18 +384,18 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable {
// ---- BEGIN diff --------------------------------------------------------------------------
public async computeDiff(originalUrl: string, modifiedUrl: string, options: IDocumentDiffProviderOptions): Promise<IDiffComputationResult | null> {
public async computeDiff(originalUrl: string, modifiedUrl: string, options: IDocumentDiffProviderOptions, algorithm: DiffAlgorithmName): Promise<IDiffComputationResult | null> {
const original = this._getModel(originalUrl);
const modified = this._getModel(modifiedUrl);
if (!original || !modified) {
return null;
}
return EditorSimpleWorker.computeDiff(original, modified, options);
return EditorSimpleWorker.computeDiff(original, modified, options, algorithm);
}
private static computeDiff(originalTextModel: ICommonModel | ITextModel, modifiedTextModel: ICommonModel | ITextModel, options: IDocumentDiffProviderOptions): IDiffComputationResult {
const diffAlgorithm: ILinesDiffComputer = options.diffAlgorithm === 'experimental' ? linesDiffComputers.experimental : linesDiffComputers.smart;
private static computeDiff(originalTextModel: ICommonModel | ITextModel, modifiedTextModel: ICommonModel | ITextModel, options: IDocumentDiffProviderOptions, algorithm: DiffAlgorithmName): IDiffComputationResult {
const diffAlgorithm: ILinesDiffComputer = algorithm === 'experimental' ? linesDiffComputers.experimental : linesDiffComputers.smart;
const originalLines = originalTextModel.getLinesContent();
const modifiedLines = modifiedTextModel.getLinesContent();

View file

@ -5,7 +5,7 @@
import { URI } from 'vs/base/common/uri';
import { IRange } from 'vs/editor/common/core/range';
import { IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider';
import { IDocumentDiff, 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';
@ -14,6 +14,8 @@ import type { EditorSimpleWorker } from 'vs/editor/common/services/editorSimpleW
export const IEditorWorkerService = createDecorator<IEditorWorkerService>('editorWorkerService');
export type DiffAlgorithmName = 'smart' | 'experimental';
export interface IEditorWorkerService {
readonly _serviceBrand: undefined;
@ -21,7 +23,7 @@ export interface IEditorWorkerService {
computedUnicodeHighlights(uri: URI, options: UnicodeHighlighterOptions, range?: IRange): Promise<IUnicodeHighlightsResult>;
/** Implementation in {@link EditorSimpleWorker.computeDiff} */
computeDiff(original: URI, modified: URI, options: IDocumentDiffProviderOptions): Promise<IDiffComputationResult | null>;
computeDiff(original: URI, modified: URI, options: IDocumentDiffProviderOptions, algorithm: DiffAlgorithmName): Promise<IDocumentDiff | null>;
canComputeDirtyDiff(original: URI, modified: URI): boolean;
computeDirtyDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean): Promise<IChange[] | null>;

View file

@ -34,6 +34,7 @@ import { EditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensi
import { IMenuItem, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry';
import { LineRange, LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
/**
* Create a new editor under `domElement`.
@ -503,6 +504,9 @@ export function createMonacoEditorAPI(): typeof monaco.editor {
TextModelResolvedOptions: <any>TextModelResolvedOptions,
FindMatch: <any>FindMatch,
ApplyUpdateResult: <any>ApplyUpdateResult,
LineRange: <any>LineRange,
LineRangeMapping: <any>LineRangeMapping,
RangeMapping: <any>RangeMapping,
// vars
EditorType: EditorType,

View file

@ -76,7 +76,7 @@ suite('standardLinesDiffCompute', () => {
}
`.split('\n');
const diff = c.computeDiff(lines1, lines2, { maxComputationTime: 1000, ignoreTrimWhitespace: false });
const diff = c.computeDiff(lines1, lines2, { maxComputationTimeMs: 1000, ignoreTrimWhitespace: false });
// TODO this diff should only have one inner, not two.
assert.deepStrictEqual(

View file

@ -5,9 +5,9 @@
import { URI } from 'vs/base/common/uri';
import { IRange } from 'vs/editor/common/core/range';
import { IDiffComputationResult, IEditorWorkerService, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorker';
import { DiffAlgorithmName, IEditorWorkerService, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorker';
import { TextEdit, IInplaceReplaceSupportResult } from 'vs/editor/common/languages';
import { IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider';
import { IDocumentDiff, IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider';
import { IChange } from 'vs/editor/common/diff/smartLinesDiffComputer';
export class TestEditorWorkerService implements IEditorWorkerService {
@ -16,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, options: IDocumentDiffProviderOptions): Promise<IDiffComputationResult | null> { return null; }
async computeDiff(original: URI, modified: URI, options: IDocumentDiffProviderOptions, algorithm: DiffAlgorithmName): Promise<IDocumentDiff | 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; }

120
src/vs/monaco.d.ts vendored
View file

@ -2235,6 +2235,124 @@ declare namespace monaco.editor {
export interface ILineChange extends IChange {
readonly charChanges: ICharChange[] | undefined;
}
/**
* A document diff provider computes the diff between two text models.
*/
export interface IDocumentDiffProvider {
/**
* Computes the diff between the text models `original` and `modified`.
*/
computeDiff(original: ITextModel, modified: ITextModel, options: IDocumentDiffProviderOptions): Promise<IDocumentDiff>;
/**
* Is fired when settings of the diff algorithm change that could alter the result of the diffing computation.
* Any user of this provider should recompute the diff when this event is fired.
*/
onDidChange: IEvent<void>;
}
/**
* Options for the diff computation.
*/
export interface IDocumentDiffProviderOptions {
/**
* When set to true, the diff should ignore whitespace changes.i
*/
ignoreTrimWhitespace: boolean;
/**
* A diff computation should throw if it takes longer than this value.
*/
maxComputationTimeMs: number;
}
/**
* Represents a diff between two text models.
*/
export interface IDocumentDiff {
/**
* If true, both text models are identical (byte-wise).
*/
readonly identical: boolean;
/**
* If true, the diff computation timed out and the diff might not be accurate.
*/
readonly quitEarly: boolean;
/**
* Maps all modified line ranges in the original to the corresponding line ranges in the modified text model.
*/
readonly changes: LineRangeMapping[];
}
/**
* Maps a line range in the original text model to a line range in the modified text model.
*/
export class LineRangeMapping {
/**
* The line range in the original text model.
*/
readonly originalRange: LineRange;
/**
* The line range in the modified text model.
*/
readonly modifiedRange: LineRange;
/**
* If inner changes have not been computed, this is set to undefined.
* Otherwise, it represents the character-level diff in this line range.
* The original range of each range mapping should be contained in the original line range (same for modified).
* Must not be an empty array.
*/
readonly innerChanges: RangeMapping[] | undefined;
constructor(originalRange: LineRange, modifiedRange: LineRange, innerChanges: RangeMapping[] | undefined);
toString(): string;
}
/**
* A range of lines (1-based).
*/
export class LineRange {
/**
* The start line number.
*/
readonly startLineNumber: number;
/**
* The end line number (exclusive).
*/
readonly endLineNumberExclusive: number;
constructor(startLineNumber: number, endLineNumberExclusive: number);
/**
* Indicates if this line range is empty.
*/
get isEmpty(): boolean;
/**
* Moves this line range by the given offset of line numbers.
*/
delta(offset: number): LineRange;
/**
* The number of lines this line range spans.
*/
get length(): number;
/**
* Creates a line range that combines this and the given line range.
*/
join(other: LineRange): LineRange;
toString(): string;
}
/**
* Maps a range in the original text model to a range in the modified text model.
*/
export class RangeMapping {
/**
* The original range.
*/
readonly originalRange: Range;
/**
* The modified range.
*/
readonly modifiedRange: Range;
constructor(originalRange: Range, modifiedRange: Range);
toString(): string;
}
export interface IDimension {
width: number;
height: number;
@ -3574,7 +3692,7 @@ declare namespace monaco.editor {
/**
* Diff Algorithm
*/
diffAlgorithm?: 'smart' | 'experimental';
diffAlgorithm?: 'smart' | 'experimental' | IDocumentDiffProvider;
}
/**

View file

@ -11,7 +11,6 @@ import { derived, IObservable, observableFromEvent, observableValue } from 'vs/b
import { basename, isEqual } from 'vs/base/common/resources';
import Severity from 'vs/base/common/severity';
import { URI } from 'vs/base/common/uri';
import { WorkerBasedDocumentDiffProvider } from 'vs/editor/browser/widget/workerBasedDocumentDiffProvider';
import { IModelService } from 'vs/editor/common/services/model';
import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService';
import { localize } from 'vs/nls';
@ -103,15 +102,14 @@ export class TempFileMergeEditorModeFactory implements IMergeEditorInputModelFac
);
store.add(temporaryResultModel);
const diffProvider = this._instantiationService.createInstance(WorkerBasedDocumentDiffProvider);
const mergeDiffComputer = this._instantiationService.createInstance(MergeDiffComputer);
const model = this._instantiationService.createInstance(
MergeEditorModel,
base.object.textEditorModel,
input1Data,
input2Data,
temporaryResultModel,
this._instantiationService.createInstance(MergeDiffComputer, diffProvider),
this._instantiationService.createInstance(MergeDiffComputer, diffProvider),
mergeDiffComputer,
{
resetResult: true,
},
@ -314,15 +312,15 @@ export class WorkspaceMergeEditorModeFactory implements IMergeEditorInputModelFa
const hasConflictMarkers = lines.some(l => l.startsWith(conflictMarkers.start));
const resetResult = hasConflictMarkers;
const diffProvider = this._instantiationService.createInstance(WorkerBasedDocumentDiffProvider);
const mergeDiffComputer = this._instantiationService.createInstance(MergeDiffComputer);
const model = this._instantiationService.createInstance(
MergeEditorModel,
base.object.textEditorModel,
input1Data,
input2Data,
result.object.textEditorModel,
this._instantiationService.createInstance(MergeDiffComputer, diffProvider),
this._instantiationService.createInstance(MergeDiffComputer, diffProvider),
mergeDiffComputer,
{
resetResult
},

View file

@ -7,9 +7,9 @@ import { assertFn, checkAdjacentItems } from 'vs/base/common/assert';
import { IReader, observableFromEvent } from 'vs/base/common/observable';
import { isDefined } from 'vs/base/common/types';
import { Range } from 'vs/editor/common/core/range';
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';
@ -23,30 +23,33 @@ export interface IMergeDiffComputerResult {
}
export class MergeDiffComputer implements IMergeDiffComputer {
private readonly mergeAlgorithm = observableFromEvent(
this.configurationService.onDidChangeConfiguration,
() => /** @description config: mergeAlgorithm.diffAlgorithm */ this.configurationService.getValue<'smart' | 'experimental'>('mergeEditor.diffAlgorithm')
);
constructor(
private readonly documentDiffProvider: IDocumentDiffProvider,
@IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService,
@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,
const result = await this.editorWorkerService.computeDiff(
textModel1.uri,
textModel2.uri,
{
ignoreTrimWhitespace: false,
maxComputationTime: 0,
diffAlgorithm,
}
maxComputationTimeMs: 0,
},
diffAlgorithm,
);
if (!result) {
throw new Error('Diff computation failed');
}
if (textModel1.isDisposed() || textModel2.isDisposed()) {
return { diffs: null };
}
@ -76,15 +79,15 @@ export class MergeDiffComputer implements IMergeDiffComputer {
}
}
function toLineRange(range: DiffLineRange): LineRange {
export function toLineRange(range: DiffLineRange): LineRange {
return new LineRange(range.startLineNumber, range.length);
}
function toRangeMapping(mapping: DiffRangeMapping): RangeMapping {
export function toRangeMapping(mapping: DiffRangeMapping): RangeMapping {
return new RangeMapping(mapping.originalRange, mapping.modifiedRange);
}
function normalizeRangeMapping(rangeMapping: RangeMapping, inputTextModel: ITextModel, outputTextModel: ITextModel): RangeMapping | undefined {
export function normalizeRangeMapping(rangeMapping: RangeMapping, inputTextModel: ITextModel, outputTextModel: ITextModel): RangeMapping | undefined {
const inputRangeEmpty = rangeMapping.inputRange.isEmpty();
const outputRangeEmpty = rangeMapping.outputRange.isEmpty();

View file

@ -29,7 +29,7 @@ export interface InputData {
export class MergeEditorModel extends EditorModel {
private readonly input1TextModelDiffs = this._register(new TextModelDiffs(this.base, this.input1.textModel, this.diffComputer));
private readonly input2TextModelDiffs = this._register(new TextModelDiffs(this.base, this.input2.textModel, this.diffComputer));
private readonly resultTextModelDiffs = this._register(new TextModelDiffs(this.base, this.resultTextModel, this.diffComputerConflictProjection));
private readonly resultTextModelDiffs = this._register(new TextModelDiffs(this.base, this.resultTextModel, this.diffComputer));
public readonly modifiedBaseRanges = derived<ModifiedBaseRange[]>('modifiedBaseRanges', (reader) => {
const input1Diffs = this.input1TextModelDiffs.diffs.read(reader);
const input2Diffs = this.input2TextModelDiffs.diffs.read(reader);
@ -54,7 +54,6 @@ export class MergeEditorModel extends EditorModel {
readonly input2: InputData,
readonly resultTextModel: ITextModel,
private readonly diffComputer: IMergeDiffComputer,
private readonly diffComputerConflictProjection: IMergeDiffComputer,
private readonly options: { resetResult: boolean },
public readonly telemetry: MergeEditorTelemetry,
@IModelService private readonly modelService: IModelService,

View file

@ -1,65 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { IDocumentDiff, IDocumentDiffProvider, IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider';
import { LineRange, LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { ITextModel } from 'vs/editor/common/model';
import { IModelService } from 'vs/editor/common/services/model';
import { TextModelProjection } from 'vs/workbench/contrib/mergeEditor/browser/model/textModelProjection';
export class ProjectedDiffComputer implements IDocumentDiffProvider {
private readonly projectedTextModel = new Map<ITextModel, TextModelProjection>();
constructor(
private readonly underlyingDiffComputer: IDocumentDiffProvider,
@IModelService private readonly modelService: IModelService,
) {
}
async computeDiff(
textModel1: ITextModel,
textModel2: ITextModel,
options: IDocumentDiffProviderOptions
): Promise<IDocumentDiff> {
let proj = this.projectedTextModel.get(textModel2);
if (!proj) {
proj = TextModelProjection.create(textModel2, {
blockToRemoveStartLinePrefix: '<<<<<<<',
blockToRemoveEndLinePrefix: '>>>>>>>',
}, this.modelService);
this.projectedTextModel.set(textModel2, proj);
}
const result = await this.underlyingDiffComputer.computeDiff(textModel1, proj.targetDocument, options);
const transformer = proj.createMonotonousReverseTransformer();
return {
identical: result.identical,
quitEarly: result.quitEarly,
changes: result.changes.map(d => {
const start = transformer.transform(new Position(d.modifiedRange.startLineNumber, 1)).lineNumber;
const innerChanges = d.innerChanges?.map(m => {
const start = transformer.transform(m.modifiedRange.getStartPosition());
const end = transformer.transform(m.modifiedRange.getEndPosition());
return new RangeMapping(m.originalRange, Range.fromPositions(start, end));
});
const end = transformer.transform(new Position(d.modifiedRange.endLineNumberExclusive, 1)).lineNumber;
return new LineRangeMapping(
d.originalRange,
new LineRange(start, end),
innerChanges
);
})
};
}
}

View file

@ -5,7 +5,8 @@
import * as assert from 'assert';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { transaction } from 'vs/base/common/observable';
import { IReader, transaction } from 'vs/base/common/observable';
import { isDefined } from 'vs/base/common/types';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import { Range } from 'vs/editor/common/core/range';
import { linesDiffComputers } from 'vs/editor/common/diff/linesDiffComputers';
@ -13,7 +14,8 @@ import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model';
import { createModelServices, createTextModel } from 'vs/editor/test/common/testTextModel';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { MergeDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer';
import { IMergeDiffComputer, IMergeDiffComputerResult, normalizeRangeMapping, toLineRange, toRangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer';
import { DetailedLineRangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping';
import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel';
import { MergeEditorTelemetry } from 'vs/workbench/contrib/mergeEditor/browser/telemetry';
@ -260,23 +262,27 @@ class MergeModelInterface extends Disposable {
const baseTextModel = this._register(createTextModel(options.base, options.languageId));
const resultTextModel = this._register(createTextModel(options.result, options.languageId));
const diffComputer = 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
};
},
const diffComputer: IMergeDiffComputer = {
async computeDiff(textModel1: ITextModel, textModel2: ITextModel, reader: IReader): Promise<IMergeDiffComputerResult> {
const result = await linesDiffComputers.smart.computeDiff(
textModel1.getLinesContent(),
textModel2.getLinesContent(),
{ ignoreTrimWhitespace: false, maxComputationTimeMs: 10000 }
);
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
};
}
);
};
this.mergeModel = this._register(instantiationService.createInstance(MergeEditorModel,
baseTextModel,
@ -294,7 +300,6 @@ class MergeModelInterface extends Disposable {
},
resultTextModel,
diffComputer,
diffComputer,
{
resetResult: false
},