Implements initial version of moved code detection. (#184336)

* Implements initial version of moved code detection.

* Fixes monaco.d.ts

* Fixes tests.
This commit is contained in:
Henning Dieterichs 2023-06-05 17:50:55 +02:00 committed by GitHub
parent feab5843d9
commit aa88e727da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1068 additions and 176 deletions

View file

@ -96,6 +96,7 @@
"--vscode-diffEditor-insertedLineBackground",
"--vscode-diffEditor-insertedTextBackground",
"--vscode-diffEditor-insertedTextBorder",
"--vscode-diffEditor-move-border",
"--vscode-diffEditor-removedLineBackground",
"--vscode-diffEditor-removedTextBackground",
"--vscode-diffEditor-removedTextBorder",

View file

@ -110,7 +110,7 @@ export interface ICommandHandler {
#include(vs/editor/common/diff/smartLinesDiffComputer): IChange, ICharChange, ILineChange
#include(vs/editor/common/diff/documentDiffProvider): IDocumentDiffProvider, IDocumentDiffProviderOptions, IDocumentDiff
#include(vs/editor/common/core/lineRange): LineRange
#include(vs/editor/common/diff/linesDiffComputer): LineRangeMapping, RangeMapping
#include(vs/editor/common/diff/linesDiffComputer): LineRangeMapping, RangeMapping, MovedText, SimpleLineRangeMapping
#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 { DiffAlgorithmName, IDiffComputationResult, IEditorWorkerService, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorker';
import { DiffAlgorithmName, IDiffComputationResult, IEditorWorkerService, ILineChange, 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';
@ -27,7 +27,7 @@ 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 { IDocumentDiff, IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider';
import { ILinesDiffComputerOptions, LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { ILinesDiffComputerOptions, LineRangeMapping, MovedText, RangeMapping, SimpleLineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { LineRange } from 'vs/editor/common/core/lineRange';
/**
@ -106,22 +106,28 @@ export class EditorWorkerService extends Disposable implements IEditorWorkerServ
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])
)
)
)
),
changes: toLineRangeMappings(result.changes),
moves: result.moves.map(m => new MovedText(
new SimpleLineRangeMapping(new LineRange(m[0], m[1]), new LineRange(m[2], m[3])),
toLineRangeMappings(m[4])
))
};
return diff;
function toLineRangeMappings(changes: readonly ILineChange[]): readonly LineRangeMapping[] {
return 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])
)
)
)
);
}
}
public canComputeDirtyDiff(original: URI, modified: URI): boolean {
@ -153,11 +159,12 @@ export class EditorWorkerService extends Disposable implements IEditorWorkerServ
return Promise.resolve(edits); // File too large
}
const sw = StopWatch.create(true);
const result = this._workerManager.withWorker().then(client => client.computeHumanReadableDiff(resource, edits, { ignoreTrimWhitespace: false, maxComputationTimeMs: 1000 })).catch((err) => {
onUnexpectedError(err);
// In case of an exception, fall back to computeMoreMinimalEdits
return this.computeMoreMinimalEdits(resource, edits, true);
});
const result = this._workerManager.withWorker().then(client => client.computeHumanReadableDiff(resource, edits,
{ ignoreTrimWhitespace: false, maxComputationTimeMs: 1000, computeMoves: false, })).catch((err) => {
onUnexpectedError(err);
// In case of an exception, fall back to computeMoreMinimalEdits
return this.computeMoreMinimalEdits(resource, edits, true);
});
result.finally(() => this._logService.trace('FORMAT#computeHumanReadableDiff', resource.toString(true), sw.elapsed()));
return result;

View file

@ -1167,6 +1167,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE
this._documentDiffProvider.computeDiff(currentOriginalModel, currentModifiedModel, {
ignoreTrimWhitespace: this._options.ignoreTrimWhitespace,
maxComputationTimeMs: this._options.maxComputationTime,
computeMoves: false,
}).then(result => {
if (currentToken === this._diffComputationToken
&& currentOriginalModel === this._originalEditor.getModel()

View 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 { localize } from 'vs/nls';
import { registerColor } from 'vs/platform/theme/common/colorRegistry';
export const diffMoveBorder = registerColor(
'diffEditor.move.border',
{ dark: '#8b8b8b9c', light: '#8b8b8b9c', hcDark: '#8b8b8b9c', hcLight: '#8b8b8b9c', },
localize('diffEditor.move.border', 'The border color for text that got moved in the diff editor.')
);

View file

@ -10,6 +10,7 @@ import { localize } from 'vs/nls';
import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import './colors';
export class ToggleCollapseUnchangedRegions extends Action2 {
constructor() {

View file

@ -8,6 +8,8 @@ import { findLast } from 'vs/base/common/arrays';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { IObservable, ISettableObservable, derived, keepAlive, observableValue, waitForState } from 'vs/base/common/observable';
import { disposableObservableValue } from 'vs/base/common/observableImpl/base';
import { isDefined } from 'vs/base/common/types';
import { Constants } from 'vs/base/common/uint';
import 'vs/css!./style';
import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration';
@ -19,14 +21,15 @@ import { IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEdito
import { diffAddDecoration, diffDeleteDecoration, diffFullLineAddDecoration, diffFullLineDeleteDecoration } from 'vs/editor/browser/widget/diffEditorWidget2/decorations';
import { DiffEditorSash } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorSash';
import { ViewZoneAlignment } from 'vs/editor/browser/widget/diffEditorWidget2/lineAlignment';
import { MovedBlocksLinesPart } from 'vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines';
import { OverviewRulerPart } from 'vs/editor/browser/widget/diffEditorWidget2/overviewRulerPart';
import { UnchangedRangesFeature } from 'vs/editor/browser/widget/diffEditorWidget2/unchangedRanges';
import { ObservableElementSizeObserver, applyObservableDecorations } from 'vs/editor/browser/widget/diffEditorWidget2/utils';
import { WorkerBasedDocumentDiffProvider } from 'vs/editor/browser/widget/workerBasedDocumentDiffProvider';
import { EditorOptions, IDiffEditorOptions, ValidDiffEditorBaseOptions, clampedFloat, clampedInt, boolean as validateBooleanOption, stringSet as validateStringSetOption } from 'vs/editor/common/config/editorOptions';
import { IDimension } from 'vs/editor/common/core/dimension';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { Position } from 'vs/editor/common/core/position';
import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { IDiffComputationResult, ILineChange } from 'vs/editor/common/diff/smartLinesDiffComputer';
import { EditorType, IContentSizeChangedEvent, IDiffEditorModel, IDiffEditorViewState } from 'vs/editor/common/editorCommon';
import { IModelDeltaDecoration } from 'vs/editor/common/model';
@ -35,7 +38,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { DelegatingEditor } from './delegatingEditorImpl';
import { DiffModel } from './diffModel';
import { DiffMapping, DiffModel } from './diffModel';
const diffEditorDefaultOptions: ValidDiffEditorBaseOptions = {
enableSplitViewResizing: true,
@ -54,6 +57,7 @@ const diffEditorDefaultOptions: ValidDiffEditorBaseOptions = {
accessibilityVerbose: false,
experimental: {
collapseUnchangedRegions: false,
showMoves: false,
}
};
@ -64,7 +68,7 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
]);
private readonly _model = observableValue<IDiffEditorModel | null>('diffEditorModel', null);
public readonly onDidChangeModel = Event.fromObservableLight(this._model);
private readonly _diffModel = observableValue<DiffModel | null>('diffModel', null);
private readonly _diffModel = this._register(disposableObservableValue<DiffModel | undefined>('diffModel', undefined));
private readonly _onDidContentSizeChange = this._register(new Emitter<IContentSizeChangedEvent>());
public readonly onDidContentSizeChange = this._onDidContentSizeChange.event;
private readonly _modifiedEditor: CodeEditorWidget;
@ -75,7 +79,6 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
);
private readonly _rootSizeObserver: ObservableElementSizeObserver;
private readonly _options: ISettableObservable<ValidDiffEditorBaseOptions>;
private _isHandlingScrollEvent = false;
private readonly _sash: DiffEditorSash;
private readonly _renderOverviewRuler: IObservable<boolean>;
@ -124,7 +127,6 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
this._register(new UnchangedRangesFeature(this._originalEditor, this._modifiedEditor, this._diffModel));
this._register(new ViewZoneAlignment(this._originalEditor, this._modifiedEditor, this._diffModel));
this._register(this._instantiationService.createInstance(OverviewRulerPart,
this._originalEditor,
this._modifiedEditor,
@ -141,6 +143,15 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
codeEditorService.addDiffEditor(this);
this._register(keepAlive(this._layoutInfo, true));
this._register(new MovedBlocksLinesPart(
this.elements.root,
this._diffModel,
this._layoutInfo.map(i => i.originalEditor),
this._layoutInfo.map(i => i.modifiedEditor),
this._originalEditor,
this._modifiedEditor,
));
}
private readonly _layoutInfo = derived('modifiedEditorLayoutInfo', (reader) => {
@ -161,7 +172,10 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
height
});
return { modifiedEditor: this._modifiedEditor.getLayoutInfo() };
return {
modifiedEditor: this._modifiedEditor.getLayoutInfo(),
originalEditor: this._originalEditor.getLayoutInfo(),
};
});
private readonly _decorations = derived('decorations', (reader) => {
@ -170,23 +184,70 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
return null;
}
const currentMove = this._diffModel.read(reader)!.syncedMovedTexts.read(reader);
const originalDecorations: IModelDeltaDecoration[] = [];
const modifiedDecorations: IModelDeltaDecoration[] = [];
for (const c of diff.changes) {
const fullRangeOriginal = c.originalRange.toInclusiveRange();
if (fullRangeOriginal) {
originalDecorations.push({ range: fullRangeOriginal, options: diffFullLineDeleteDecoration });
}
const fullRangeModified = c.modifiedRange.toInclusiveRange();
if (fullRangeModified) {
modifiedDecorations.push({ range: fullRangeModified, options: diffFullLineAddDecoration });
for (const m of diff.mappings) {
const fullRangeOriginal = LineRange.subtract(m.lineRangeMapping.originalRange, currentMove?.lineRangeMapping.originalRange)
.map(i => i.toInclusiveRange()).filter(isDefined);
for (const range of fullRangeOriginal) {
originalDecorations.push({ range, options: diffFullLineDeleteDecoration });
}
for (const i of c.innerChanges || []) {
const fullRangeModified = LineRange.subtract(m.lineRangeMapping.modifiedRange, currentMove?.lineRangeMapping.modifiedRange)
.map(i => i.toInclusiveRange()).filter(isDefined);
for (const range of fullRangeModified) {
modifiedDecorations.push({ range, options: diffFullLineAddDecoration });
}
for (const i of m.lineRangeMapping.innerChanges || []) {
if (currentMove
&& (currentMove.lineRangeMapping.originalRange.intersect(new LineRange(i.originalRange.startLineNumber, i.originalRange.endLineNumber))
|| currentMove.lineRangeMapping.modifiedRange.intersect(new LineRange(i.modifiedRange.startLineNumber, i.modifiedRange.endLineNumber)))) {
continue;
}
originalDecorations.push({ range: i.originalRange, options: diffDeleteDecoration });
modifiedDecorations.push({ range: i.modifiedRange, options: diffAddDecoration });
}
}
if (currentMove) {
for (const m of currentMove.changes) {
const fullRangeOriginal = m.originalRange.toInclusiveRange();
if (fullRangeOriginal) {
originalDecorations.push({ range: fullRangeOriginal, options: diffFullLineDeleteDecoration });
}
const fullRangeModified = m.modifiedRange.toInclusiveRange();
if (fullRangeModified) {
modifiedDecorations.push({ range: fullRangeModified, options: diffFullLineAddDecoration });
}
for (const i of m.innerChanges || []) {
originalDecorations.push({ range: i.originalRange, options: diffDeleteDecoration });
modifiedDecorations.push({ range: i.modifiedRange, options: diffAddDecoration });
}
}
}
for (const m of diff.movedTexts) {
originalDecorations.push({
range: m.lineRangeMapping.originalRange.toInclusiveRange()!, options: {
description: 'moved',
blockClassName: 'movedOriginal',
blockPadding: [MovedBlocksLinesPart.movedCodeBlockPadding, 0, MovedBlocksLinesPart.movedCodeBlockPadding, MovedBlocksLinesPart.movedCodeBlockPadding],
}
});
modifiedDecorations.push({
range: m.lineRangeMapping.modifiedRange.toInclusiveRange()!, options: {
description: 'moved',
blockClassName: 'movedModified',
blockPadding: [4, 0, 4, 4],
}
});
}
return { originalDecorations, modifiedDecorations };
});
@ -202,19 +263,34 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
}
private _createLeftHandSideEditor(options: Readonly<IDiffEditorConstructionOptions>, codeEditorWidgetOptions: ICodeEditorWidgetOptions): CodeEditorWidget {
const editor = this._createInnerEditor(this._instantiationService, this.elements.original, this._adjustOptionsForLeftHandSide(options), codeEditorWidgetOptions);
const editor = this._constructInnerEditor(this._instantiationService, this.elements.original, this._adjustOptionsForLeftHandSide(options), codeEditorWidgetOptions);
const isInDiffLeftEditorKey = this._contextKeyService.createKey<boolean>('isInDiffLeftEditor', editor.hasWidgetFocus());
this._register(editor.onDidFocusEditorWidget(() => isInDiffLeftEditorKey.set(true)));
this._register(editor.onDidBlurEditorWidget(() => isInDiffLeftEditorKey.set(false)));
this._register(editor.onDidChangeCursorPosition(e => {
const m = this._diffModel.get();
if (!m) { return; }
const movedText = m.diff.get()!.movedTexts.find(m => m.lineRangeMapping.originalRange.contains(e.position.lineNumber));
m.syncedMovedTexts.set(movedText, undefined);
}));
return editor;
}
private _createRightHandSideEditor(options: Readonly<IDiffEditorConstructionOptions>, codeEditorWidgetOptions: ICodeEditorWidgetOptions): CodeEditorWidget {
const editor = this._createInnerEditor(this._instantiationService, this.elements.modified, this._adjustOptionsForRightHandSide(options), codeEditorWidgetOptions);
const editor = this._constructInnerEditor(this._instantiationService, this.elements.modified, this._adjustOptionsForRightHandSide(options), codeEditorWidgetOptions);
const isInDiffRightEditorKey = this._contextKeyService.createKey<boolean>('isInDiffRightEditor', editor.hasWidgetFocus());
this._register(editor.onDidFocusEditorWidget(() => isInDiffRightEditorKey.set(true)));
this._register(editor.onDidBlurEditorWidget(() => isInDiffRightEditorKey.set(false)));
this._register(editor.onDidChangeCursorPosition(e => {
const m = this._diffModel.get();
if (!m) { return; }
const movedText = m.diff.get()!.movedTexts.find(m => m.lineRangeMapping.modifiedRange.contains(e.position.lineNumber));
m.syncedMovedTexts.set(movedText, undefined);
}));
// Revert change when an arrow is clicked.
/*TODO
this._register(editor.onMouseDown(event => {
@ -238,8 +314,8 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
return editor;
}
protected _createInnerEditor(instantiationService: IInstantiationService, container: HTMLElement, options: Readonly<IEditorConstructionOptions>, editorWidgetOptions: ICodeEditorWidgetOptions): CodeEditorWidget {
const editor = instantiationService.createInstance(CodeEditorWidget, container, options, editorWidgetOptions);
protected _constructInnerEditor(instantiationService: IInstantiationService, container: HTMLElement, options: Readonly<IEditorConstructionOptions>, editorWidgetOptions: ICodeEditorWidgetOptions): CodeEditorWidget {
const editor = this._createInnerEditor(instantiationService, container, options, editorWidgetOptions);
this._register(editor.onDidContentSizeChange(e => {
const width = this._originalEditor.getContentWidth() + this._modifiedEditor.getContentWidth() + OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH;
@ -252,32 +328,17 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
contentWidthChanged: e.contentWidthChanged
});
}));
return editor;
}
this._register(editor.onDidScrollChange((e) => {
if (this._isHandlingScrollEvent) {
return;
}
if (!e.scrollTopChanged && !e.scrollLeftChanged && !e.scrollHeightChanged) {
return;
}
this._isHandlingScrollEvent = true;
try {
const otherEditor = editor === this._originalEditor ? this._modifiedEditor : this._originalEditor;
otherEditor.setScrollPosition({
scrollLeft: e.scrollLeft,
scrollTop: e.scrollTop
});
} finally {
this._isHandlingScrollEvent = false;
}
}));
protected _createInnerEditor(instantiationService: IInstantiationService, container: HTMLElement, options: Readonly<IEditorConstructionOptions>, editorWidgetOptions: ICodeEditorWidgetOptions): CodeEditorWidget {
const editor = instantiationService.createInstance(CodeEditorWidget, container, options, editorWidgetOptions);
return editor;
}
private _adjustOptionsForLeftHandSide(options: Readonly<IDiffEditorConstructionOptions>): IEditorConstructionOptions {
const result = this._adjustOptionsForSubEditor(options);
if (!options.renderSideBySide) {
if (!this._options.get().renderSideBySide) {
// never wrap hidden editor
result.wordWrapOverride1 = 'off';
result.wordWrapOverride2 = 'off';
@ -289,7 +350,7 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
result.ariaLabel = options.originalAriaLabel;
}
result.ariaLabel = this._updateAriaLabel(result.ariaLabel);
result.readOnly = !options.originalEditable;
result.readOnly = !this._options.get().originalEditable;
result.dropIntoEditor = { enabled: !result.readOnly };
result.extraEditorClassName = 'original-in-monaco-diff-editor';
return result;
@ -386,8 +447,9 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
this._options.map(o => o.ignoreTrimWhitespace),
this._options.map(o => o.maxComputationTime),
this._options.map(o => o.experimental.collapseUnchangedRegions!),
this._options.map(o => o.experimental.showMoves!),
this._instantiationService.createInstance(WorkerBasedDocumentDiffProvider, this._options.get())
) : null, undefined);
) : undefined, undefined);
}
override updateOptions(_newOptions: IDiffEditorOptions): void {
@ -439,24 +501,24 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
//throw new Error('Method not implemented.');
}
private _goTo(diff: LineRangeMapping): void {
this._modifiedEditor.setPosition(new Position(diff.modifiedRange.startLineNumber, 1));
this._modifiedEditor.revealRangeInCenter(diff.modifiedRange.toExclusiveRange());
private _goTo(diff: DiffMapping): void {
this._modifiedEditor.setPosition(new Position(diff.lineRangeMapping.modifiedRange.startLineNumber, 1));
this._modifiedEditor.revealRangeInCenter(diff.lineRangeMapping.modifiedRange.toExclusiveRange());
}
goToDiff(target: 'previous' | 'next'): void {
const diffs = this._diffModel.get()?.diff.get()?.changes;
const diffs = this._diffModel.get()?.diff.get()?.mappings;
if (!diffs || diffs.length === 0) {
return;
}
const curLineNumber = this._modifiedEditor.getPosition()!.lineNumber;
let diff: LineRangeMapping | undefined;
let diff: DiffMapping | undefined;
if (target === 'next') {
diff = diffs.find(d => d.modifiedRange.startLineNumber > curLineNumber) ?? diffs[0];
diff = diffs.find(d => d.lineRangeMapping.modifiedRange.startLineNumber > curLineNumber) ?? diffs[0];
} else {
diff = findLast(diffs, d => d.modifiedRange.startLineNumber < curLineNumber) ?? diffs[diffs.length - 1];
diff = findLast(diffs, d => d.lineRangeMapping.modifiedRange.startLineNumber < curLineNumber) ?? diffs[diffs.length - 1];
}
this._goTo(diff);
}
@ -468,7 +530,7 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
}
// wait for the diff computation to finish
waitForState(diffModel.isDiffUpToDate, s => s).then(() => {
const diffs = diffModel.diff.get()?.changes;
const diffs = diffModel.diff.get()?.mappings;
if (!diffs || diffs.length === 0) {
return;
}
@ -495,6 +557,7 @@ function validateDiffEditorOptions(options: Readonly<IDiffEditorOptions>, defaul
accessibilityVerbose: validateBooleanOption(options.accessibilityVerbose, defaults.accessibilityVerbose),
experimental: {
collapseUnchangedRegions: validateBooleanOption(options.experimental?.collapseUnchangedRegions, defaults.experimental.collapseUnchangedRegions!),
showMoves: validateBooleanOption(options.experimental?.showMoves, defaults.experimental.showMoves!),
},
};
}

View file

@ -10,7 +10,7 @@ import { autorunWithStore2 } from 'vs/base/common/observableImpl/autorun';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { Range } from 'vs/editor/common/core/range';
import { IDocumentDiff, IDocumentDiffProvider } from 'vs/editor/common/diff/documentDiffProvider';
import { LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { LineRangeMapping, MovedText, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { lineRangeMappingFromRangeMappings } from 'vs/editor/common/diff/standardLinesDiffComputer';
import { IDiffEditorModel } from 'vs/editor/common/editorCommon';
import { ITextModel } from 'vs/editor/common/model';
@ -22,35 +22,39 @@ export class DiffModel extends Disposable {
private readonly _isDiffUpToDate = observableValue<boolean>('isDiffUpToDate', false);
public readonly isDiffUpToDate: IObservable<boolean> = this._isDiffUpToDate;
private readonly _diff = observableValue<IDocumentDiff | undefined>('diff', undefined);
public readonly diff: IObservable<IDocumentDiff | undefined> = this._diff;
private readonly _diff = observableValue<DiffState | undefined>('diff', undefined);
public readonly diff: IObservable<DiffState | undefined> = this._diff;
private readonly _unchangedRegions = observableValue<{ regions: UnchangedRegion[]; originalDecorationIds: string[]; modifiedDecorationIds: string[] }>('unchangedRegion', { regions: [], originalDecorationIds: [], modifiedDecorationIds: [] });
public readonly unchangedRegions: IObservable<UnchangedRegion[]> = derived('unchangedRegions', r =>
this.hideUnchangedRegions.read(r) ? this._unchangedRegions.read(r).regions : []
this._hideUnchangedRegions.read(r) ? this._unchangedRegions.read(r).regions : []
);
public readonly syncedMovedTexts = observableValue<MovedText | undefined>('syncedMovedText', undefined);
constructor(
model: IDiffEditorModel,
ignoreTrimWhitespace: IObservable<boolean>,
maxComputationTimeMs: IObservable<number>,
private readonly hideUnchangedRegions: IObservable<boolean>,
private readonly _hideUnchangedRegions: IObservable<boolean>,
private readonly _showMoves: IObservable<boolean>,
documentDiffProvider: IDocumentDiffProvider,
) {
super();
const contentChangedSignal = observableSignal('contentChangedSignal');
const debouncer = this._register(new RunOnceScheduler(() => contentChangedSignal.trigger(undefined), 200));
this._register(model.modified.onDidChangeContent((e) => {
const diff = this._diff.get();
if (!diff) {
return;
}
const textEdits = TextEditInfo.fromModelContentChanges(e.changes);
/*const textEdits = TextEditInfo.fromModelContentChanges(e.changes);
this._diff.set(
applyModifiedEdits(diff, textEdits, model.original, model.modified),
undefined
);
);*/
debouncer.schedule();
}));
this._register(model.original.onDidChangeContent((e) => {
@ -58,11 +62,11 @@ export class DiffModel extends Disposable {
if (!diff) {
return;
}
const textEdits = TextEditInfo.fromModelContentChanges(e.changes);
/*const textEdits = TextEditInfo.fromModelContentChanges(e.changes);
this._diff.set(
applyOriginalEdits(diff, textEdits, model.original, model.modified),
undefined
);
);*/
debouncer.schedule();
}));
@ -71,10 +75,7 @@ export class DiffModel extends Disposable {
this._register(autorunWithStore2('compute diff', async (reader, store) => {
debouncer.cancel();
contentChangedSignal.read(reader);
documentDiffProviderOptionChanged.read(reader);
const ignoreTrimWhitespaceVal = ignoreTrimWhitespace.read(reader);
const maxComputationTimeMsVal = maxComputationTimeMs.read(reader);
this._isDiffUpToDate.set(false, undefined);
@ -91,8 +92,9 @@ export class DiffModel extends Disposable {
}));
let result = await documentDiffProvider.computeDiff(model.original, model.modified, {
ignoreTrimWhitespace: ignoreTrimWhitespaceVal,
maxComputationTimeMs: maxComputationTimeMsVal,
ignoreTrimWhitespace: ignoreTrimWhitespace.read(reader),
maxComputationTimeMs: maxComputationTimeMs.read(reader),
computeMoves: this._showMoves.read(reader),
});
result = applyOriginalEdits(result, originalTextEditInfos, model.original, model.modified);
@ -135,7 +137,7 @@ export class DiffModel extends Disposable {
);
transaction(tx => {
this._diff.set(result, tx);
this._diff.set(DiffState.fromDiffResult(result), tx);
this._isDiffUpToDate.set(true, tx);
this._unchangedRegions.set(
@ -171,8 +173,47 @@ export class DiffModel extends Disposable {
}
}
export class DiffState {
public static fromDiffResult(result: IDocumentDiff): DiffState {
return new DiffState(
result.changes.map(c => new DiffMapping(c)),
result.moves || []
);
}
constructor(
public readonly mappings: readonly DiffMapping[],
public readonly movedTexts: readonly MovedText[],
) { }
}
export class DiffMapping {
constructor(
readonly lineRangeMapping: LineRangeMapping,
) {
/*
readonly movedTo: MovedText | undefined,
readonly movedFrom: MovedText | undefined,
if (movedTo) {
assertFn(() =>
movedTo.lineRangeMapping.modifiedRange.equals(lineRangeMapping.modifiedRange)
&& lineRangeMapping.originalRange.isEmpty
&& !movedFrom
);
} else if (movedFrom) {
assertFn(() =>
movedFrom.lineRangeMapping.originalRange.equals(lineRangeMapping.originalRange)
&& lineRangeMapping.modifiedRange.isEmpty
&& !movedTo
);
}
*/
}
}
export class UnchangedRegion {
public static fromDiffs(changes: LineRangeMapping[], originalLineCount: number, modifiedLineCount: number): UnchangedRegion[] {
public static fromDiffs(changes: readonly LineRangeMapping[], originalLineCount: number, modifiedLineCount: number): UnchangedRegion[] {
const inversedMappings = LineRangeMapping.inverse(changes, originalLineCount, modifiedLineCount);
const result: UnchangedRegion[] = [];
@ -301,6 +342,7 @@ function applyOriginalEdits(diff: IDocumentDiff, textEdits: TextEditInfo[], orig
identical: false,
quitEarly: false,
changes,
moves: [],
};
}
@ -340,5 +382,6 @@ function applyModifiedEdits(diff: IDocumentDiff, textEdits: TextEditInfo[], orig
identical: false,
quitEarly: false,
changes,
moves: [],
};
}

View file

@ -5,22 +5,33 @@
import { ArrayQueue } from 'vs/base/common/arrays';
import { Disposable } from 'vs/base/common/lifecycle';
import { IObservable, observableSignalFromEvent, derived } from 'vs/base/common/observable';
import { autorunWithStore2 } from 'vs/base/common/observableImpl/autorun';
import { IObservable, observableSignalFromEvent, derived, observableValue, observableFromEvent } from 'vs/base/common/observable';
import { autorun, autorunWithStore2 } from 'vs/base/common/observableImpl/autorun';
import { IViewZone } from 'vs/editor/browser/editorBrowser';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { DiffModel } from 'vs/editor/browser/widget/diffEditorWidget2/diffModel';
import { joinCombine } from 'vs/editor/browser/widget/diffEditorWidget2/utils';
import { DiffMapping, DiffModel } from 'vs/editor/browser/widget/diffEditorWidget2/diffModel';
import { animatedObservable, joinCombine } from 'vs/editor/browser/widget/diffEditorWidget2/utils';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { Position } from 'vs/editor/common/core/position';
import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { ScrollType } from 'vs/editor/common/editorCommon';
export class ViewZoneAlignment extends Disposable {
private readonly _origExtraHeight = observableValue('origExtraHeight', 0);
private readonly _modExtraHeight = observableValue('modExtraHeight', 0);
private readonly _originalScrollTop: IObservable<number>;
private readonly _originalScrollOffset = observableValue<number, boolean>('originalScrollOffset', 0);
private readonly _originalScrollOffsetAnimated = animatedObservable(this._originalScrollOffset, this._store);
private readonly _modifiedScrollTop: IObservable<number>;
private readonly _modifiedScrollOffset = observableValue<number, boolean>('modifiedScrollOffset', 0);
private readonly _modifiedScrollOffsetAnimated = animatedObservable(this._modifiedScrollOffset, this._store);
constructor(
private readonly _originalEditor: CodeEditorWidget,
private readonly _modifiedEditor: CodeEditorWidget,
private readonly _diffModel: IObservable<DiffModel | null>,
private readonly _diffModel: IObservable<DiffModel | undefined>,
) {
super();
@ -39,13 +50,28 @@ export class ViewZoneAlignment extends Disposable {
const alignmentViewZoneIdsMod = new Set<string>();
const alignments = derived<IRangeAlignment[] | null>('alignments', (reader) => {
const diff = this._diffModel.read(reader)?.diff.read(reader);
if (!diff) { return null; }
const diffModel = this._diffModel.read(reader);
const diff = diffModel?.diff.read(reader);
if (!diffModel || !diff) { return null; }
origViewZonesChanged.read(reader);
modViewZonesChanged.read(reader);
return computeRangeAlignment(this._originalEditor, this._modifiedEditor, diff.changes, alignmentViewZoneIdsOrig, alignmentViewZoneIdsMod);
return computeRangeAlignment(this._originalEditor, this._modifiedEditor, diff.mappings, alignmentViewZoneIdsOrig, alignmentViewZoneIdsMod);
});
const alignmentsSyncedMovedText = derived<IRangeAlignment[] | null>('alignments', (reader) => {
origViewZonesChanged.read(reader);
modViewZonesChanged.read(reader);
const syncedMovedText = this._diffModel.read(reader)?.syncedMovedTexts.read(reader);
if (!syncedMovedText) {
return null;
}
const mappings = syncedMovedText.changes.map(c => new DiffMapping(c));
// TOD dont include alignments outside syncedMovedText
return computeRangeAlignment(this._originalEditor, this._modifiedEditor, mappings, alignmentViewZoneIdsOrig, alignmentViewZoneIdsMod);
});
function createFakeLinesDiv(): HTMLElement {
@ -60,16 +86,43 @@ export class ViewZoneAlignment extends Disposable {
const origViewZones: IViewZone[] = [];
const modViewZones: IViewZone[] = [];
const _modExtraHeight = this._modExtraHeight.read(reader);
if (_modExtraHeight > 0) {
modViewZones.push({
afterLineNumber: 0,
domNode: document.createElement('div'),
heightInPx: _modExtraHeight,
});
}
const _origExtraHeight = this._origExtraHeight.read(reader);
if (_origExtraHeight > 0) {
origViewZones.push({
afterLineNumber: 0,
domNode: document.createElement('div'),
heightInPx: _origExtraHeight,
});
}
const syncedMovedText = this._diffModel.read(reader)?.syncedMovedTexts.read(reader);
if (alignments_) {
for (const a of alignments_) {
const delta = a.modifiedHeightInPx - a.originalHeightInPx;
if (delta > 0) {
if (syncedMovedText?.lineRangeMapping.originalRange.contains(a.originalRange.endLineNumberExclusive - 1)) {
continue;
}
origViewZones.push({
afterLineNumber: a.originalRange.endLineNumberExclusive - 1,
domNode: createFakeLinesDiv(),
heightInPx: delta,
});
} else {
if (syncedMovedText?.lineRangeMapping.modifiedRange.contains(a.modifiedRange.endLineNumberExclusive - 1)) {
continue;
}
modViewZones.push({
afterLineNumber: a.modifiedRange.endLineNumberExclusive - 1,
domNode: createFakeLinesDiv(),
@ -79,6 +132,28 @@ export class ViewZoneAlignment extends Disposable {
}
}
for (const a of alignmentsSyncedMovedText.read(reader) ?? []) {
if (!syncedMovedText?.lineRangeMapping.originalRange.intersect(a.originalRange)
&& !syncedMovedText?.lineRangeMapping.modifiedRange.intersect(a.modifiedRange)) {
// ignore unrelated alignments outside the synced moved text
continue;
}
const delta = a.modifiedHeightInPx - a.originalHeightInPx;
if (delta > 0) {
origViewZones.push({
afterLineNumber: a.originalRange.endLineNumberExclusive - 1,
domNode: createFakeLinesDiv(),
heightInPx: delta,
});
} else {
modViewZones.push({
afterLineNumber: a.modifiedRange.endLineNumberExclusive - 1,
domNode: createFakeLinesDiv(),
heightInPx: -delta,
});
}
}
return { orig: origViewZones, mod: modViewZones };
});
@ -98,47 +173,66 @@ export class ViewZoneAlignment extends Disposable {
isChangingViewZones = false;
}));
}
}
this._originalScrollTop = observableFromEvent(this._originalEditor.onDidScrollChange, () => this._originalEditor.getScrollTop());
this._modifiedScrollTop = observableFromEvent(this._modifiedEditor.onDidScrollChange, () => this._modifiedEditor.getScrollTop());
interface AdditionalLineHeightInfo {
lineNumber: number;
heightInPx: number;
}
// origExtraHeight + origOffset - origScrollTop = modExtraHeight + modOffset - modScrollTop
function getAdditionalLineHeights(editor: CodeEditorWidget, viewZonesToIgnore: ReadonlySet<string>): readonly AdditionalLineHeightInfo[] {
const viewZoneHeights: { lineNumber: number; heightInPx: number }[] = [];
const wrappingZoneHeights: { lineNumber: number; heightInPx: number }[] = [];
// origScrollTop = origExtraHeight + origOffset - modExtraHeight - modOffset + modScrollTop
// modScrollTop = modExtraHeight + modOffset - origExtraHeight - origOffset + origScrollTop
const hasWrapping = editor.getOption(EditorOption.wrappingInfo).wrappingColumn !== -1;
const coordinatesConverter = editor._getViewModel()!.coordinatesConverter;
if (hasWrapping) {
for (let i = 1; i <= editor.getModel()!.getLineCount(); i++) {
const lineCount = coordinatesConverter.getModelLineViewLineCount(i);
if (lineCount > 1) {
wrappingZoneHeights.push({ lineNumber: i, heightInPx: lineCount - 1 });
// origOffset - modOffset = heightOfLines(1..Y) - heightOfLines(1..X)
// origScrollTop >= 0, modScrollTop >= 0
this._register(autorun('update scroll modified', (reader) => {
const newScrollTopModified = this._originalScrollTop.read(reader)
- (this._originalScrollOffsetAnimated.get() - this._modifiedScrollOffsetAnimated.read(reader))
- (this._origExtraHeight.get() - this._modExtraHeight.read(reader));
if (newScrollTopModified !== this._modifiedEditor.getScrollTop()) {
this._modifiedEditor.setScrollTop(newScrollTopModified, ScrollType.Immediate);
}
}
}));
this._register(autorun('update scroll original', (reader) => {
const newScrollTopOriginal = this._modifiedScrollTop.read(reader)
- (this._modifiedScrollOffsetAnimated.get() - this._originalScrollOffsetAnimated.read(reader))
- (this._modExtraHeight.get() - this._origExtraHeight.read(reader));
if (newScrollTopOriginal !== this._originalEditor.getScrollTop()) {
this._originalEditor.setScrollTop(newScrollTopOriginal, ScrollType.Immediate);
}
}));
this._register(autorun('update', reader => {
const m = this._diffModel.read(reader)?.syncedMovedTexts.read(reader);
let deltaOrigToMod = 0;
if (m) {
const trueTopOriginal = this._originalEditor.getTopForLineNumber(m.lineRangeMapping.originalRange.startLineNumber, true) - this._origExtraHeight.get();
const trueTopModified = this._modifiedEditor.getTopForLineNumber(m.lineRangeMapping.modifiedRange.startLineNumber, true) - this._modExtraHeight.get();
deltaOrigToMod = trueTopModified - trueTopOriginal;
}
if (deltaOrigToMod > 0) {
this._modExtraHeight.set(0, undefined);
this._origExtraHeight.set(deltaOrigToMod, undefined);
} else if (deltaOrigToMod < 0) {
this._modExtraHeight.set(-deltaOrigToMod, undefined);
this._origExtraHeight.set(0, undefined);
} else {
setTimeout(() => {
this._modExtraHeight.set(0, undefined);
this._origExtraHeight.set(0, undefined);
}, 400);
}
if (this._modifiedEditor.hasTextFocus()) {
this._originalScrollOffset.set(this._modifiedScrollOffset.get() - deltaOrigToMod, undefined, true);
} else {
this._modifiedScrollOffset.set(this._originalScrollOffset.get() + deltaOrigToMod, undefined, true);
}
}));
}
for (const w of editor.getWhitespaces()) {
if (viewZonesToIgnore.has(w.id)) {
continue;
}
const modelLineNumber = coordinatesConverter.convertViewPositionToModelPosition(
new Position(w.afterLineNumber, 1)
).lineNumber;
viewZoneHeights.push({ lineNumber: modelLineNumber, heightInPx: w.height });
}
const result = joinCombine(
viewZoneHeights,
wrappingZoneHeights,
v => v.lineNumber,
(v1, v2) => ({ lineNumber: v1.lineNumber, heightInPx: v1.heightInPx + v2.heightInPx })
);
return result;
}
interface IRangeAlignment {
@ -153,7 +247,7 @@ interface IRangeAlignment {
function computeRangeAlignment(
originalEditor: CodeEditorWidget,
modifiedEditor: CodeEditorWidget,
diffs: LineRangeMapping[],
diffs: readonly DiffMapping[],
originalEditorAlignmentViewZones: ReadonlySet<string>,
modifiedEditorAlignmentViewZones: ReadonlySet<string>,
): IRangeAlignment[] {
@ -211,7 +305,8 @@ function computeRangeAlignment(
}
}
for (const c of diffs) {
for (const m of diffs) {
const c = m.lineRangeMapping;
handleAlignmentsOutsideOfDiffs(c.originalRange.startLineNumber, c.modifiedRange.startLineNumber);
const originalAdditionalHeight = originalLineHeightOverrides
@ -235,3 +330,44 @@ function computeRangeAlignment(
return result;
}
interface AdditionalLineHeightInfo {
lineNumber: number;
heightInPx: number;
}
function getAdditionalLineHeights(editor: CodeEditorWidget, viewZonesToIgnore: ReadonlySet<string>): readonly AdditionalLineHeightInfo[] {
const viewZoneHeights: { lineNumber: number; heightInPx: number }[] = [];
const wrappingZoneHeights: { lineNumber: number; heightInPx: number }[] = [];
const hasWrapping = editor.getOption(EditorOption.wrappingInfo).wrappingColumn !== -1;
const coordinatesConverter = editor._getViewModel()!.coordinatesConverter;
const editorLineHeight = editor.getOption(EditorOption.lineHeight);
if (hasWrapping) {
for (let i = 1; i <= editor.getModel()!.getLineCount(); i++) {
const lineCount = coordinatesConverter.getModelLineViewLineCount(i);
if (lineCount > 1) {
wrappingZoneHeights.push({ lineNumber: i, heightInPx: editorLineHeight * (lineCount - 1) });
}
}
}
for (const w of editor.getWhitespaces()) {
if (viewZonesToIgnore.has(w.id)) {
continue;
}
const modelLineNumber = coordinatesConverter.convertViewPositionToModelPosition(
new Position(w.afterLineNumber, 1)
).lineNumber;
viewZoneHeights.push({ lineNumber: modelLineNumber, heightInPx: w.height });
}
const result = joinCombine(
viewZoneHeights,
wrappingZoneHeights,
v => v.lineNumber,
(v1, v2) => ({ lineNumber: v1.lineNumber, heightInPx: v1.heightInPx + v2.heightInPx })
);
return result;
}

View file

@ -0,0 +1,88 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { IObservable, autorun, observableFromEvent } from 'vs/base/common/observable';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { DiffModel } from 'vs/editor/browser/widget/diffEditorWidget2/diffModel';
import { EditorLayoutInfo } from 'vs/editor/common/config/editorOptions';
import { LineRange } from 'vs/editor/common/core/lineRange';
export class MovedBlocksLinesPart extends Disposable {
public static readonly movedCodeBlockPadding = 4;
constructor(
private readonly _rootElement: HTMLElement,
private readonly _diffModel: IObservable<DiffModel | undefined>,
private readonly _originalEditorLayoutInfo: IObservable<EditorLayoutInfo | null>,
private readonly _modifiedEditorLayoutInfo: IObservable<EditorLayoutInfo | null>,
private readonly _originalEditor: ICodeEditor,
private readonly _modifiedEditor: ICodeEditor,
) {
super();
const element = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
element.setAttribute('class', 'moved-blocks-lines');
this._rootElement.appendChild(element);
this._register(autorun('update', (reader) => {
const info = this._originalEditorLayoutInfo.read(reader);
const info2 = this._modifiedEditorLayoutInfo.read(reader);
if (!info || !info2) {
return;
}
element.style.left = `${info.width - info.verticalScrollbarWidth}px`;
element.style.height = `${info.height}px`;
element.style.width = `${info.verticalScrollbarWidth + info.contentLeft - MovedBlocksLinesPart.movedCodeBlockPadding}px`;
}));
const originalScrollTop = observableFromEvent(this._originalEditor.onDidScrollChange, () => this._originalEditor.getScrollTop());
const modifiedScrollTop = observableFromEvent(this._modifiedEditor.onDidScrollChange, () => this._modifiedEditor.getScrollTop());
this._register(autorun('update', (reader) => {
const info = this._originalEditorLayoutInfo.read(reader);
const info2 = this._modifiedEditorLayoutInfo.read(reader);
if (!info || !info2) {
return;
}
const width = info.verticalScrollbarWidth + info.contentLeft - MovedBlocksLinesPart.movedCodeBlockPadding;
const moves = this._diffModel.read(reader)?.diff.read(reader)?.movedTexts;
if (!moves) {
return;
}
element.replaceChildren();
let idx = 0;
for (const m of moves) {
function computeLineStart(range: LineRange, editor: ICodeEditor) {
const t1 = editor.getTopForLineNumber(range.startLineNumber);
const t2 = editor.getTopForLineNumber(range.endLineNumberExclusive);
return (t1 + t2) / 2;
}
const start = computeLineStart(m.lineRangeMapping.originalRange, this._originalEditor);
const startOffset = originalScrollTop.read(reader);
const end = computeLineStart(m.lineRangeMapping.modifiedRange, this._modifiedEditor);
const endOffset = modifiedScrollTop.read(reader);
const top = start - startOffset;
const bottom = end - endOffset;
const center = (width / 2) - moves.length * 5 + idx * 10;
idx++;
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', `M ${0} ${top} L ${center} ${top} L ${center} ${bottom} L ${width} ${bottom}`);
path.setAttribute('fill', 'none');
element.appendChild(path);
}
}));
}
}

View file

@ -27,7 +27,7 @@ export class OverviewRulerPart extends Disposable {
private readonly _originalEditor: CodeEditorWidget,
private readonly _modifiedEditor: CodeEditorWidget,
private readonly _rootElement: HTMLElement,
private readonly _diffModel: IObservable<DiffModel | null>,
private readonly _diffModel: IObservable<DiffModel | undefined>,
private readonly _rootWidth: IObservable<number>,
private readonly _rootHeight: IObservable<number>,
private readonly _modifiedEditorLayoutInfo: IObservable<EditorLayoutInfo | null>,
@ -92,7 +92,7 @@ export class OverviewRulerPart extends Disposable {
store.add(autorun('set overview ruler zones', (reader) => {
const colors = currentColors.read(reader);
const diff = m?.diff.read(reader)?.changes;
const diff = m?.diff.read(reader)?.mappings;
function createZones(ranges: LineRange[], color: Color) {
return ranges
@ -100,8 +100,8 @@ export class OverviewRulerPart extends Disposable {
.map(r => new OverviewRulerZone(r.startLineNumber, r.endLineNumberExclusive, r.length, color.toString()));
}
originalOverviewRuler?.setZones(createZones((diff || []).map(d => d.originalRange), colors.removeColor));
modifiedOverviewRuler?.setZones(createZones((diff || []).map(d => d.modifiedRange), colors.insertColor));
originalOverviewRuler?.setZones(createZones((diff || []).map(d => d.lineRangeMapping.originalRange), colors.removeColor));
modifiedOverviewRuler?.setZones(createZones((diff || []).map(d => d.lineRangeMapping.modifiedRange), colors.insertColor));
}));
store.add(autorun('layout overview ruler', (reader) => {

View file

@ -59,3 +59,22 @@
.merge-editor-conflict-actions > a:hover .codicon::before {
cursor: pointer;
}
.movedOriginal {
border: 2px solid var(--vscode-diffEditor-move-border);
}
.movedModified {
border: 2px solid var(--vscode-diffEditor-move-border);
}
.moved-blocks-lines {
position: absolute;
pointer-events: none;
}
.moved-blocks-lines path {
fill: none;
stroke: var(--vscode-diffEditor-move-border);
stroke-width: 2;
}

View file

@ -17,7 +17,7 @@ export class UnchangedRangesFeature extends Disposable {
constructor(
private readonly _originalEditor: CodeEditorWidget,
private readonly _modifiedEditor: CodeEditorWidget,
private readonly _diffModel: IObservable<DiffModel | null>,
private readonly _diffModel: IObservable<DiffModel | undefined>,
) {
super();

View file

@ -5,7 +5,7 @@
import { IDimension } from 'vs/base/browser/dom';
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { IObservable, ISettableObservable, autorun, observableFromEvent, observableValue, transaction } from 'vs/base/common/observable';
import { IObservable, ISettableObservable, autorun, autorunHandleChanges, observableFromEvent, observableValue, transaction } from 'vs/base/common/observable';
import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IModelDeltaDecoration } from 'vs/editor/common/model';
@ -119,3 +119,54 @@ export class ObservableElementSizeObserver extends Disposable {
}
}
}
export function animatedObservable(base: IObservable<number, boolean>, store: DisposableStore): IObservable<number> {
let targetVal = base.get();
let startVal = targetVal;
let curVal = targetVal;
const result = observableValue('animatedValue', targetVal);
let animationStartMs: number = -1;
const durationMs = 300;
let animationFrame: number | undefined = undefined;
store.add(autorunHandleChanges('update value', {
createEmptyChangeSummary: () => ({ animate: false }),
handleChange: (ctx, s) => {
if (ctx.didChange(base)) {
s.animate = s.animate || ctx.change;
}
return true;
}
}, (reader, s) => {
if (animationFrame !== undefined) {
cancelAnimationFrame(animationFrame);
animationFrame = undefined;
}
startVal = curVal;
targetVal = base.read(reader);
animationStartMs = Date.now() - (s.animate ? 0 : durationMs);
update();
}));
function update() {
const passedMs = Date.now() - animationStartMs;
curVal = Math.floor(easeOutExpo(passedMs, startVal, targetVal - startVal, durationMs));
if (passedMs < durationMs) {
animationFrame = requestAnimationFrame(update);
} else {
curVal = targetVal;
}
result.set(curVal, undefined);
}
return result;
}
function easeOutExpo(t: number, b: number, c: number, d: number): number {
return t === d ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;
}

View file

@ -54,6 +54,7 @@ export class WorkerBasedDocumentDiffProvider implements IDocumentDiffProvider, I
],
identical: false,
quitEarly: false,
moves: [],
};
}

View file

@ -207,6 +207,11 @@ const editorConfiguration: IConfigurationNode = {
default: false,
description: nls.localize('collapseUnchangedRegions', "Controls whether the diff editor shows unchanged regions. Only works when 'diffEditor.experimental.useVersion2' is set."),
},
'diffEditor.experimental.showMoves': {
type: 'boolean',
default: false,
description: nls.localize('showMoves', "Controls whether the diff editor should show detected code moves. Only works when 'diffEditor.experimental.useVersion2' is set."),
},
'diffEditor.experimental.useVersion2': {
type: 'boolean',
default: false,

View file

@ -804,6 +804,10 @@ export interface IDiffEditorBaseOptions {
* Defaults to false.
*/
collapseUnchangedRegions?: boolean;
/**
* Defaults to false.
*/
showMoves?: boolean;
};
}

View file

@ -14,6 +14,24 @@ export class LineRange {
return new LineRange(range.startLineNumber, range.endLineNumber);
}
public static subtract(a: LineRange, b: LineRange | undefined): LineRange[] {
if (!b) {
return [a];
}
if (a.startLineNumber < b.startLineNumber && b.endLineNumberExclusive < a.endLineNumberExclusive) {
return [
new LineRange(a.startLineNumber, b.startLineNumber),
new LineRange(b.endLineNumberExclusive, a.endLineNumberExclusive)
];
} else if (b.startLineNumber <= a.startLineNumber && a.endLineNumberExclusive <= b.endLineNumberExclusive) {
return [];
} else if (b.endLineNumberExclusive < a.endLineNumberExclusive) {
return [new LineRange(Math.max(b.endLineNumberExclusive, a.startLineNumber), a.endLineNumberExclusive)];
} else {
return [new LineRange(a.startLineNumber, Math.min(b.startLineNumber, a.endLineNumberExclusive))];
}
}
/**
* @param lineRanges An array of sorted line ranges.
*/

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { LineRangeMapping, MovedText } from 'vs/editor/common/diff/linesDiffComputer';
import { ITextModel } from 'vs/editor/common/model';
/**
@ -36,6 +36,11 @@ export interface IDocumentDiffProviderOptions {
* A diff computation should throw if it takes longer than this value.
*/
maxComputationTimeMs: number;
/**
* If set, the diff computation should compute moves in addition to insertions and deletions.
*/
computeMoves: boolean;
}
/**
@ -55,5 +60,11 @@ export interface IDocumentDiff {
/**
* Maps all modified line ranges in the original to the corresponding line ranges in the modified text model.
*/
readonly changes: LineRangeMapping[];
readonly changes: readonly LineRangeMapping[];
/**
* Sorted by original line ranges.
* The original line ranges and the modified line ranges must be disjoint (but can be touching).
*/
readonly moves: readonly MovedText[];
}

View file

@ -13,12 +13,19 @@ export interface ILinesDiffComputer {
export interface ILinesDiffComputerOptions {
readonly ignoreTrimWhitespace: boolean;
readonly maxComputationTimeMs: number;
readonly computeMoves: boolean;
}
export class LinesDiff {
constructor(
readonly changes: readonly LineRangeMapping[],
/**
* Sorted by original line ranges.
* The original line ranges and the modified line ranges must be disjoint (but can be touching).
*/
readonly moves: readonly MovedText[],
/**
* Indicates if the time out was reached.
* In that case, the diffs might be an approximation and the user should be asked to rerun the diff with more time.
@ -32,7 +39,7 @@ export class LinesDiff {
* Maps a line range in the original text model to a line range in the modified text model.
*/
export class LineRangeMapping {
public static inverse(mapping: LineRangeMapping[], originalLineCount: number, modifiedLineCount: number): LineRangeMapping[] {
public static inverse(mapping: readonly LineRangeMapping[], originalLineCount: number, modifiedLineCount: number): LineRangeMapping[] {
const result: LineRangeMapping[] = [];
let lastOriginalEndLineNumber = 1;
let lastModifiedEndLineNumber = 1;
@ -124,3 +131,34 @@ export class RangeMapping {
return `{${this.originalRange.toString()}->${this.modifiedRange.toString()}}`;
}
}
export class SimpleLineRangeMapping {
constructor(
public readonly originalRange: LineRange,
public readonly modifiedRange: LineRange,
) {
}
public toString(): string {
return `{${this.originalRange.toString()}->${this.modifiedRange.toString()}}`;
}
}
export class MovedText {
public readonly lineRangeMapping: SimpleLineRangeMapping;
/**
* The diff from the original text to the moved text.
* Must be contained in the original/modified line range.
* Can be empty if the text didn't change (only moved).
*/
public readonly changes: readonly LineRangeMapping[];
constructor(
lineRangeMapping: SimpleLineRangeMapping,
changes: readonly LineRangeMapping[],
) {
this.lineRangeMapping = lineRangeMapping;
this.changes = changes;
}
}

View file

@ -75,7 +75,7 @@ export class SmartLinesDiffComputer implements ILinesDiffComputer {
);
});
return new LinesDiff(changes, result.quitEarly);
return new LinesDiff(changes, [], result.quitEarly);
}
}
@ -91,7 +91,7 @@ export interface IDiffComputationResult {
/**
* The changes as (modern) line range mapping array.
*/
changes2: LineRangeMapping[];
changes2: readonly LineRangeMapping[];
}
/**

View file

@ -13,7 +13,7 @@ import { DateTimeout, ISequence, ITimeout, InfiniteTimeout, SequenceDiff } from
import { DynamicProgrammingDiffing } from 'vs/editor/common/diff/algorithms/dynamicProgrammingDiffing';
import { optimizeSequenceDiffs, smoothenSequenceDiffs } from 'vs/editor/common/diff/algorithms/joinSequenceDiffs';
import { MyersDiffAlgorithm } from 'vs/editor/common/diff/algorithms/myersDiffAlgorithm';
import { ILinesDiffComputer, ILinesDiffComputerOptions, LineRangeMapping, LinesDiff, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { ILinesDiffComputer, ILinesDiffComputerOptions, LineRangeMapping, LinesDiff, MovedText, RangeMapping, SimpleLineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
export class StandardLinesDiffComputer implements ILinesDiffComputer {
private readonly dynamicProgrammingDiffing = new DynamicProgrammingDiffing();
@ -116,7 +116,41 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer {
scanForWhitespaceChanges(originalLines.length - seq1LastStart);
const changes = lineRangeMappingFromRangeMappings(alignments, originalLines, modifiedLines);
return new LinesDiff(changes, hitTimeout);
const moves: MovedText[] = [];
if (options.computeMoves) {
const deletions = changes
.filter(c => c.modifiedRange.isEmpty && c.originalRange.length >= 3)
.map(d => new LineRangeFragment(d.originalRange, originalLines));
const insertions = new Set(changes
.filter(c => c.originalRange.isEmpty && c.modifiedRange.length >= 3)
.map(d => new LineRangeFragment(d.modifiedRange, modifiedLines)));
for (const deletion of deletions) {
let highestSimilarity = -1;
let best: LineRangeFragment | undefined;
for (const insertion of insertions) {
const similarity = deletion.computeSimilarity(insertion);
if (similarity > highestSimilarity) {
highestSimilarity = similarity;
best = insertion;
}
}
if (highestSimilarity > 0.90 && best) {
const moveChanges = this.refineDiff(originalLines, modifiedLines, new SequenceDiff(
new OffsetRange(deletion.range.startLineNumber - 1, deletion.range.endLineNumberExclusive - 1),
new OffsetRange(best.range.startLineNumber - 1, best.range.endLineNumberExclusive - 1),
), timeout, considerWhitespaceChanges);
const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, originalLines, modifiedLines);
insertions.delete(best);
moves.push(new MovedText(new SimpleLineRangeMapping(deletion.range, best.range), mappings));
}
}
}
return new LinesDiff(changes, moves, hitTimeout);
}
private refineDiff(originalLines: string[], modifiedLines: string[], diff: SequenceDiff, timeout: ITimeout, considerWhitespaceChanges: boolean): { mappings: RangeMapping[]; hitTimeout: boolean } {
@ -467,8 +501,8 @@ class Slice implements ISequence {
}
}
const offsetOfPrevLineBreak = i === 0 ? 0 : this.firstCharOffsetByLineMinusOne[i - 1];
return new Position(this.lineRange.start + i + 1, offset - offsetOfPrevLineBreak + 1 + this.offsetByLine[i]);
const offsetOfFirstCharInLine = i === 0 ? 0 : this.firstCharOffsetByLineMinusOne[i - 1];
return new Position(this.lineRange.start + i + 1, offset - offsetOfFirstCharInLine + 1 + this.offsetByLine[i]);
}
public translateRange(range: OffsetRange): Range {
@ -558,3 +592,47 @@ function getCategory(charCode: number): CharBoundaryCategory {
function isSpace(charCode: number): boolean {
return charCode === CharCode.Space || charCode === CharCode.Tab;
}
const chrKeys = new Map<string, number>();
function getKey(chr: string): number {
let key = chrKeys.get(chr);
if (key === undefined) {
key = chrKeys.size;
chrKeys.set(chr, key);
}
return key;
}
class LineRangeFragment {
private readonly totalCount: number;
private readonly histogram: number[] = [];
constructor(
public readonly range: LineRange,
public readonly lines: string[],
) {
let counter = 0;
for (let i = range.startLineNumber - 1; i < range.endLineNumberExclusive - 1; i++) {
const line = lines[i];
for (let j = 0; j < line.length; j++) {
counter++;
const chr = line[j];
const key = getKey(chr);
this.histogram[key] = (this.histogram[key] || 0) + 1;
}
counter++;
const key = getKey('\n');
this.histogram[key] = (this.histogram[key] || 0) + 1;
}
this.totalCount = counter;
}
public computeSimilarity(other: LineRangeFragment): number {
let sumDifferences = 0;
const maxLength = Math.max(this.histogram.length, other.histogram.length);
for (let i = 0; i < maxLength; i++) {
sumDifferences += Math.abs((this.histogram[i] ?? 0) - (other.histogram[i] ?? 0));
}
return 1 - (sumDifferences / (this.totalCount + other.totalCount));
}
}

View file

@ -15,13 +15,13 @@ import { ensureValidWordDefinition, getWordAtText, IWordAtPosition } from 'vs/ed
import { IColorInformation, 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 { DiffAlgorithmName, IDiffComputationResult, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorker';
import { DiffAlgorithmName, IDiffComputationResult, ILineChange, 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, ILinesDiffComputerOptions } from 'vs/editor/common/diff/linesDiffComputer';
import { ILinesDiffComputer, ILinesDiffComputerOptions, LineRangeMapping } 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';
@ -422,10 +422,8 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable {
const identical = (result.changes.length > 0 ? false : this._modelsAreIdentical(originalTextModel, modifiedTextModel));
return {
identical,
quitEarly: result.hitTimeout,
changes: result.changes.map(m => ([m.originalRange.startLineNumber, m.originalRange.endLineNumberExclusive, m.modifiedRange.startLineNumber, m.modifiedRange.endLineNumberExclusive, m.innerChanges?.map(m => [
function getLineChanges(changes: readonly LineRangeMapping[]): ILineChange[] {
return 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,
@ -434,7 +432,20 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable {
m.modifiedRange.startColumn,
m.modifiedRange.endLineNumber,
m.modifiedRange.endColumn,
])]))
])]));
}
return {
identical,
quitEarly: result.hitTimeout,
changes: getLineChanges(result.changes),
moves: result.moves.map(m => ([
m.lineRangeMapping.originalRange.startLineNumber,
m.lineRangeMapping.originalRange.endLineNumberExclusive,
m.lineRangeMapping.modifiedRange.startLineNumber,
m.lineRangeMapping.modifiedRange.endLineNumberExclusive,
getLineChanges(m.changes)
])),
};
}

View file

@ -42,6 +42,7 @@ export interface IDiffComputationResult {
quitEarly: boolean;
changes: ILineChange[];
identical: boolean;
moves: ITextMove[];
}
export type ILineChange = [
@ -64,6 +65,14 @@ export type ICharChange = [
modifiedEndColumn: number,
];
export type ITextMove = [
originalStartLine: number,
originalEndLine: number,
modifiedStartLine: number,
modifiedEndLine: number,
changes: ILineChange[],
];
export interface IUnicodeHighlightsResult {
ranges: IRange[];
hasMore: boolean;

View file

@ -34,7 +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 { LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { LineRangeMapping, MovedText, RangeMapping, SimpleLineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { EditorZoom } from 'vs/editor/common/config/editorZoom';
import { IOpenerService } from 'vs/platform/opener/common/opener';
@ -586,6 +586,8 @@ export function createMonacoEditorAPI(): typeof monaco.editor {
LineRangeMapping: <any>LineRangeMapping,
RangeMapping: <any>RangeMapping,
EditorZoom: <any>EditorZoom,
MovedText: <any>MovedText,
SimpleLineRangeMapping: <any>SimpleLineRangeMapping,
// vars
EditorType: EditorType,

View file

@ -149,7 +149,7 @@ suite('EditorSimpleWorker', () => {
const smallerEdits = await worker.computeHumanReadableDiff(
model.uri.toString(),
edits,
{ ignoreTrimWhitespace: false, maxComputationTimeMs: 0 }
{ ignoreTrimWhitespace: false, maxComputationTimeMs: 0, computeMoves: false }
);
const t1 = applyEdits(model.getValue(), edits);

View file

@ -7,6 +7,7 @@ import * as assert from 'assert';
import { readdirSync, readFileSync, existsSync, writeFileSync, rmSync } from 'fs';
import { join, resolve } from 'path';
import { FileAccess } from 'vs/base/common/network';
import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { SmartLinesDiffComputer } from 'vs/editor/common/diff/smartLinesDiffComputer';
import { StandardLinesDiffComputer } from 'vs/editor/common/diff/standardLinesDiffComputer';
@ -31,20 +32,32 @@ suite('diff fixtures', () => {
const diffingAlgo = diffingAlgoName === 'legacy' ? new SmartLinesDiffComputer() : new StandardLinesDiffComputer();
const diff = diffingAlgo.computeDiff(firstContentLines, secondContentLines, { ignoreTrimWhitespace: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER });
const diff = diffingAlgo.computeDiff(firstContentLines, secondContentLines, { ignoreTrimWhitespace: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, computeMoves: false });
const actualDiffingResult: DiffingResult = {
original: { content: firstContent, fileName: `./${firstFileName}` },
modified: { content: secondContent, fileName: `./${secondFileName}` },
diffs: diff.changes.map<IDetailedDiff>(c => ({
function getDiffs(changes: readonly LineRangeMapping[]): IDetailedDiff[] {
return changes.map<IDetailedDiff>(c => ({
originalRange: c.originalRange.toString(),
modifiedRange: c.modifiedRange.toString(),
innerChanges: c.innerChanges?.map<IDiff>(c => ({
originalRange: c.originalRange.toString(),
modifiedRange: c.modifiedRange.toString(),
})) || null
}));
}
const actualDiffingResult: DiffingResult = {
original: { content: firstContent, fileName: `./${firstFileName}` },
modified: { content: secondContent, fileName: `./${secondFileName}` },
diffs: getDiffs(diff.changes),
moves: diff.moves.map(v => ({
originalRange: v.lineRangeMapping.originalRange.toString(),
modifiedRange: v.lineRangeMapping.modifiedRange.toString(),
changes: getDiffs(v.changes),
}))
};
if (actualDiffingResult.moves?.length === 0) {
delete actualDiffingResult.moves;
}
const expectedFilePath = join(folderPath, `${diffingAlgoName}.expected.diff.json`);
const invalidFilePath = join(folderPath, `${diffingAlgoName}.invalid.diff.json`);
@ -90,8 +103,8 @@ suite('diff fixtures', () => {
}
}
test(`uiae`, () => {
runTest('subword', 'advanced');
test(`test`, () => {
runTest('move-1', 'advanced');
});
for (const folder of folders) {
@ -108,6 +121,7 @@ interface DiffingResult {
modified: { content: string; fileName: string };
diffs: IDetailedDiff[];
moves?: IMoveInfo[];
}
interface IDetailedDiff {
@ -120,3 +134,10 @@ interface IDiff {
originalRange: string; // [1,18 -> 1,19]
modifiedRange: string; // [1,18 -> 1,19]
}
interface IMoveInfo {
originalRange: string; // [startLineNumber, endLineNumberExclusive)
modifiedRange: string; // [startLineNumber, endLineNumberExclusive)
changes?: IDetailedDiff[];
}

View file

@ -0,0 +1,92 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as arrays from 'vs/base/common/arrays';
import { IdleDeadline, runWhenIdle } from 'vs/base/common/async';
import { BugIndicatingError, onUnexpectedError } from 'vs/base/common/errors';
import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { setTimeout0 } from 'vs/base/common/platform';
import { StopWatch } from 'vs/base/common/stopwatch';
import { countEOL } from 'vs/editor/common/core/eolCounter';
import { Position } from 'vs/editor/common/core/position';
import { IRange } from 'vs/editor/common/core/range';
import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes';
import { EncodedTokenizationResult, IBackgroundTokenizationStore, IBackgroundTokenizer, ILanguageIdCodec, IState, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/languages';
import { nullTokenizeEncoded } from 'vs/editor/common/languages/nullTokenize';
import { ITextModel } from 'vs/editor/common/model';
import { TextModel } from 'vs/editor/common/model/textModel';
import { TokenizationTextModelPart } from 'vs/editor/common/model/tokenizationTextModelPart';
import { IModelContentChangedEvent, IModelLanguageChangedEvent } from 'vs/editor/common/textModelEvents';
import { ContiguousMultilineTokensBuilder } from 'vs/editor/common/tokens/contiguousMultilineTokensBuilder';
import { LineTokens } from 'vs/editor/common/tokens/lineTokens';
const enum Constants {
CHEAP_TOKENIZATION_LENGTH_LIMIT = 2048
}
/**
* An array that avoids being sparse by always
* filling up unused indices with a default value.
*/
export class ContiguousGrowingArray<T> {
private _store: T[] = [];
constructor(
private readonly _default: T
) { }
public get(index: number): T {
if (index < this._store.length) {
return this._store[index];
}
return this._default;
}
public set(index: number, value: T): void {
while (index >= this._store.length) {
this._store[this._store.length] = this._default;
}
this._store[index] = value;
}
// TODO have `replace` instead of `delete` and `insert`
public delete(deleteIndex: number, deleteCount: number): void {
if (deleteCount === 0 || deleteIndex >= this._store.length) {
return;
}
this._store.splice(deleteIndex, deleteCount);
}
public insert(insertIndex: number, insertCount: number): void {
if (insertCount === 0 || insertIndex >= this._store.length) {
return;
}
const arr: T[] = [];
for (let i = 0; i < insertCount; i++) {
arr[i] = this._default;
}
this._store = arrays.arrayInsert(this._store, insertIndex, arr);
}
}
/**
* Stores the states at the start of each line and keeps track of which lines
* must be re-tokenized. Also uses state equality to quickly validate lines
* that don't need to be re-tokenized.
*
* For example, when typing on a line, the line gets marked as needing to be tokenized.
* Once the line is tokenized, the end state is checked for equality against the begin
* state of the next line. If the states are equal, tokenization doesn't need to run
* again over the rest of the file. If the states are not equal, the next line gets marked
* as needing to be tokenized.
*/
export class TokenizationStateStore {
requestTokens(startLineNumber: number, endLineNumberExclusive: number): void {
for (let lineNumber = startLineNumber; lineNumber < endLineNumberExclusive; lineNumber++) {
this._stateStore.markMustBeTokenized(lineNumber - 1);
}
}
}

View file

@ -0,0 +1,92 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as arrays from 'vs/base/common/arrays';
import { IdleDeadline, runWhenIdle } from 'vs/base/common/async';
import { BugIndicatingError, onUnexpectedError } from 'vs/base/common/errors';
import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { setTimeout0 } from 'vs/base/common/platform';
import { StopWatch } from 'vs/base/common/stopwatch';
import { countEOL } from 'vs/editor/common/core/eolCounter';
import { Position } from 'vs/editor/common/core/position';
import { IRange } from 'vs/editor/common/core/range';
import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes';
import { EncodedTokenizationResult, IBackgroundTokenizationStore, IBackgroundTokenizer, ILanguageIdCodec, IState, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/languages';
import { nullTokenizeEncoded } from 'vs/editor/common/languages/nullTokenize';
import { ITextModel } from 'vs/editor/common/model';
import { TextModel } from 'vs/editor/common/model/textModel';
import { TokenizationTextModelPart } from 'vs/editor/common/model/tokenizationTextModelPart';
import { IModelContentChangedEvent, IModelLanguageChangedEvent } from 'vs/editor/common/textModelEvents';
import { ContiguousMultilineTokensBuilder } from 'vs/editor/common/tokens/contiguousMultilineTokensBuilder';
import { LineTokens } from 'vs/editor/common/tokens/lineTokens';
/**
* An array that avoids being sparse by always
* filling up unused indices with a default value.
*/
export class ContiguousGrowingArray<T> {
private _store: T[] = [];
constructor(
private readonly _default: T
) { }
public get(index: number): T {
if (index < this._store.length) {
return this._store[index];
}
return this._default;
}
public set(index: number, value: T): void {
while (index >= this._store.length) {
this._store[this._store.length] = this._default;
}
this._store[index] = value;
}
// TODO have `replace` instead of `delete` and `insert`
public delete(deleteIndex: number, deleteCount: number): void {
if (deleteCount === 0 || deleteIndex >= this._store.length) {
return;
}
this._store.splice(deleteIndex, deleteCount);
}
public insert(insertIndex: number, insertCount: number): void {
if (insertCount === 0 || insertIndex >= this._store.length) {
return;
}
const arr: T[] = [];
for (let i = 0; i < insertCount; i++) {
arr[i] = this._default;
}
this._store = arrays.arrayInsert(this._store, insertIndex, arr);
}
}
const enum Constants {
CHEAP_TOKENIZATION_LENGTH_LIMIT = 1024
}
/**
* Stores the states at the start of each line and keeps track of which lines
* must be re-tokenized. Also uses state equality to quickly validate lines
* that don't need to be re-tokenized.
*
* For example, when typing on a line, the line gets marked as needing to be tokenized.
* Once the line is tokenized, the end state is checked for equality against the begin
* state of the next line. If the states are equal, tokenization doesn't need to run
* again over the rest of the file. If the states are not equal, the next line gets marked
* as needing to be tokenized.
*/
export class TokenizationStateStore {
requestTokens(startLineNumber: number, endLineNumberExclusive: number): void {
for (let lineNumber = startLineNumber; lineNumber < endLineNumberExclusive; lineNumber++) {
this._stateStore.markMustBeTokenized(lineNumber - 1);
}
}
}

View file

@ -0,0 +1,32 @@
{
"original": {
"content": "/*---------------------------------------------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Licensed under the MIT License. See License.txt in the project root for license information.\n *--------------------------------------------------------------------------------------------*/\n\nimport * as arrays from 'vs/base/common/arrays';\nimport { IdleDeadline, runWhenIdle } from 'vs/base/common/async';\nimport { BugIndicatingError, onUnexpectedError } from 'vs/base/common/errors';\nimport { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';\nimport { setTimeout0 } from 'vs/base/common/platform';\nimport { StopWatch } from 'vs/base/common/stopwatch';\nimport { countEOL } from 'vs/editor/common/core/eolCounter';\nimport { Position } from 'vs/editor/common/core/position';\nimport { IRange } from 'vs/editor/common/core/range';\nimport { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes';\nimport { EncodedTokenizationResult, IBackgroundTokenizationStore, IBackgroundTokenizer, ILanguageIdCodec, IState, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/languages';\nimport { nullTokenizeEncoded } from 'vs/editor/common/languages/nullTokenize';\nimport { ITextModel } from 'vs/editor/common/model';\nimport { TextModel } from 'vs/editor/common/model/textModel';\nimport { TokenizationTextModelPart } from 'vs/editor/common/model/tokenizationTextModelPart';\nimport { IModelContentChangedEvent, IModelLanguageChangedEvent } from 'vs/editor/common/textModelEvents';\nimport { ContiguousMultilineTokensBuilder } from 'vs/editor/common/tokens/contiguousMultilineTokensBuilder';\nimport { LineTokens } from 'vs/editor/common/tokens/lineTokens';\n\nconst enum Constants {\n\tCHEAP_TOKENIZATION_LENGTH_LIMIT = 2048\n}\n\n/**\n * An array that avoids being sparse by always\n * filling up unused indices with a default value.\n */\nexport class ContiguousGrowingArray<T> {\n\n\tprivate _store: T[] = [];\n\n\tconstructor(\n\t\tprivate readonly _default: T\n\t) { }\n\n\tpublic get(index: number): T {\n\t\tif (index < this._store.length) {\n\t\t\treturn this._store[index];\n\t\t}\n\t\treturn this._default;\n\t}\n\n\tpublic set(index: number, value: T): void {\n\t\twhile (index >= this._store.length) {\n\t\t\tthis._store[this._store.length] = this._default;\n\t\t}\n\t\tthis._store[index] = value;\n\t}\n\n\t// TODO have `replace` instead of `delete` and `insert`\n\tpublic delete(deleteIndex: number, deleteCount: number): void {\n\t\tif (deleteCount === 0 || deleteIndex >= this._store.length) {\n\t\t\treturn;\n\t\t}\n\t\tthis._store.splice(deleteIndex, deleteCount);\n\t}\n\n\tpublic insert(insertIndex: number, insertCount: number): void {\n\t\tif (insertCount === 0 || insertIndex >= this._store.length) {\n\t\t\treturn;\n\t\t}\n\t\tconst arr: T[] = [];\n\t\tfor (let i = 0; i < insertCount; i++) {\n\t\t\tarr[i] = this._default;\n\t\t}\n\t\tthis._store = arrays.arrayInsert(this._store, insertIndex, arr);\n\t}\n}\n\n/**\n * Stores the states at the start of each line and keeps track of which lines\n * must be re-tokenized. Also uses state equality to quickly validate lines\n * that don't need to be re-tokenized.\n *\n * For example, when typing on a line, the line gets marked as needing to be tokenized.\n * Once the line is tokenized, the end state is checked for equality against the begin\n * state of the next line. If the states are equal, tokenization doesn't need to run\n * again over the rest of the file. If the states are not equal, the next line gets marked\n * as needing to be tokenized.\n */\nexport class TokenizationStateStore {\n\trequestTokens(startLineNumber: number, endLineNumberExclusive: number): void {\n\t\tfor (let lineNumber = startLineNumber; lineNumber < endLineNumberExclusive; lineNumber++) {\n\t\t\tthis._stateStore.markMustBeTokenized(lineNumber - 1);\n\t\t}\n\t}\n}\n",
"fileName": "./1.tst"
},
"modified": {
"content": "/*---------------------------------------------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Licensed under the MIT License. See License.txt in the project root for license information.\n *--------------------------------------------------------------------------------------------*/\n\nimport * as arrays from 'vs/base/common/arrays';\nimport { IdleDeadline, runWhenIdle } from 'vs/base/common/async';\nimport { BugIndicatingError, onUnexpectedError } from 'vs/base/common/errors';\nimport { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';\nimport { setTimeout0 } from 'vs/base/common/platform';\nimport { StopWatch } from 'vs/base/common/stopwatch';\nimport { countEOL } from 'vs/editor/common/core/eolCounter';\nimport { Position } from 'vs/editor/common/core/position';\nimport { IRange } from 'vs/editor/common/core/range';\nimport { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes';\nimport { EncodedTokenizationResult, IBackgroundTokenizationStore, IBackgroundTokenizer, ILanguageIdCodec, IState, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/languages';\nimport { nullTokenizeEncoded } from 'vs/editor/common/languages/nullTokenize';\nimport { ITextModel } from 'vs/editor/common/model';\nimport { TextModel } from 'vs/editor/common/model/textModel';\nimport { TokenizationTextModelPart } from 'vs/editor/common/model/tokenizationTextModelPart';\nimport { IModelContentChangedEvent, IModelLanguageChangedEvent } from 'vs/editor/common/textModelEvents';\nimport { ContiguousMultilineTokensBuilder } from 'vs/editor/common/tokens/contiguousMultilineTokensBuilder';\nimport { LineTokens } from 'vs/editor/common/tokens/lineTokens';\n\n/**\n * An array that avoids being sparse by always\n * filling up unused indices with a default value.\n */\nexport class ContiguousGrowingArray<T> {\n\n\tprivate _store: T[] = [];\n\n\tconstructor(\n\t\tprivate readonly _default: T\n\t) { }\n\n\tpublic get(index: number): T {\n\t\tif (index < this._store.length) {\n\t\t\treturn this._store[index];\n\t\t}\n\t\treturn this._default;\n\t}\n\n\tpublic set(index: number, value: T): void {\n\t\twhile (index >= this._store.length) {\n\t\t\tthis._store[this._store.length] = this._default;\n\t\t}\n\t\tthis._store[index] = value;\n\t}\n\n\t// TODO have `replace` instead of `delete` and `insert`\n\tpublic delete(deleteIndex: number, deleteCount: number): void {\n\t\tif (deleteCount === 0 || deleteIndex >= this._store.length) {\n\t\t\treturn;\n\t\t}\n\t\tthis._store.splice(deleteIndex, deleteCount);\n\t}\n\n\tpublic insert(insertIndex: number, insertCount: number): void {\n\t\tif (insertCount === 0 || insertIndex >= this._store.length) {\n\t\t\treturn;\n\t\t}\n\t\tconst arr: T[] = [];\n\t\tfor (let i = 0; i < insertCount; i++) {\n\t\t\tarr[i] = this._default;\n\t\t}\n\t\tthis._store = arrays.arrayInsert(this._store, insertIndex, arr);\n\t}\n}\n\nconst enum Constants {\n\tCHEAP_TOKENIZATION_LENGTH_LIMIT = 1024\n}\n\n/**\n * Stores the states at the start of each line and keeps track of which lines\n * must be re-tokenized. Also uses state equality to quickly validate lines\n * that don't need to be re-tokenized.\n *\n * For example, when typing on a line, the line gets marked as needing to be tokenized.\n * Once the line is tokenized, the end state is checked for equality against the begin\n * state of the next line. If the states are equal, tokenization doesn't need to run\n * again over the rest of the file. If the states are not equal, the next line gets marked\n * as needing to be tokenized.\n */\nexport class TokenizationStateStore {\n\trequestTokens(startLineNumber: number, endLineNumberExclusive: number): void {\n\t\tfor (let lineNumber = startLineNumber; lineNumber < endLineNumberExclusive; lineNumber++) {\n\t\t\tthis._stateStore.markMustBeTokenized(lineNumber - 1);\n\t\t}\n\t}\n}\n",
"fileName": "./2.tst"
},
"diffs": [
{
"originalRange": "[24,28)",
"modifiedRange": "[24,24)",
"innerChanges": [
{
"originalRange": "[24,1 -> 28,1]",
"modifiedRange": "[24,1 -> 24,1]"
}
]
},
{
"originalRange": "[74,74)",
"modifiedRange": "[70,74)",
"innerChanges": [
{
"originalRange": "[74,1 -> 74,1]",
"modifiedRange": "[70,1 -> 74,1]"
}
]
}
]
}

View file

@ -0,0 +1,22 @@
{
"original": {
"content": "/*---------------------------------------------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Licensed under the MIT License. See License.txt in the project root for license information.\n *--------------------------------------------------------------------------------------------*/\n\nimport * as arrays from 'vs/base/common/arrays';\nimport { IdleDeadline, runWhenIdle } from 'vs/base/common/async';\nimport { BugIndicatingError, onUnexpectedError } from 'vs/base/common/errors';\nimport { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';\nimport { setTimeout0 } from 'vs/base/common/platform';\nimport { StopWatch } from 'vs/base/common/stopwatch';\nimport { countEOL } from 'vs/editor/common/core/eolCounter';\nimport { Position } from 'vs/editor/common/core/position';\nimport { IRange } from 'vs/editor/common/core/range';\nimport { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes';\nimport { EncodedTokenizationResult, IBackgroundTokenizationStore, IBackgroundTokenizer, ILanguageIdCodec, IState, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/languages';\nimport { nullTokenizeEncoded } from 'vs/editor/common/languages/nullTokenize';\nimport { ITextModel } from 'vs/editor/common/model';\nimport { TextModel } from 'vs/editor/common/model/textModel';\nimport { TokenizationTextModelPart } from 'vs/editor/common/model/tokenizationTextModelPart';\nimport { IModelContentChangedEvent, IModelLanguageChangedEvent } from 'vs/editor/common/textModelEvents';\nimport { ContiguousMultilineTokensBuilder } from 'vs/editor/common/tokens/contiguousMultilineTokensBuilder';\nimport { LineTokens } from 'vs/editor/common/tokens/lineTokens';\n\nconst enum Constants {\n\tCHEAP_TOKENIZATION_LENGTH_LIMIT = 2048\n}\n\n/**\n * An array that avoids being sparse by always\n * filling up unused indices with a default value.\n */\nexport class ContiguousGrowingArray<T> {\n\n\tprivate _store: T[] = [];\n\n\tconstructor(\n\t\tprivate readonly _default: T\n\t) { }\n\n\tpublic get(index: number): T {\n\t\tif (index < this._store.length) {\n\t\t\treturn this._store[index];\n\t\t}\n\t\treturn this._default;\n\t}\n\n\tpublic set(index: number, value: T): void {\n\t\twhile (index >= this._store.length) {\n\t\t\tthis._store[this._store.length] = this._default;\n\t\t}\n\t\tthis._store[index] = value;\n\t}\n\n\t// TODO have `replace` instead of `delete` and `insert`\n\tpublic delete(deleteIndex: number, deleteCount: number): void {\n\t\tif (deleteCount === 0 || deleteIndex >= this._store.length) {\n\t\t\treturn;\n\t\t}\n\t\tthis._store.splice(deleteIndex, deleteCount);\n\t}\n\n\tpublic insert(insertIndex: number, insertCount: number): void {\n\t\tif (insertCount === 0 || insertIndex >= this._store.length) {\n\t\t\treturn;\n\t\t}\n\t\tconst arr: T[] = [];\n\t\tfor (let i = 0; i < insertCount; i++) {\n\t\t\tarr[i] = this._default;\n\t\t}\n\t\tthis._store = arrays.arrayInsert(this._store, insertIndex, arr);\n\t}\n}\n\n/**\n * Stores the states at the start of each line and keeps track of which lines\n * must be re-tokenized. Also uses state equality to quickly validate lines\n * that don't need to be re-tokenized.\n *\n * For example, when typing on a line, the line gets marked as needing to be tokenized.\n * Once the line is tokenized, the end state is checked for equality against the begin\n * state of the next line. If the states are equal, tokenization doesn't need to run\n * again over the rest of the file. If the states are not equal, the next line gets marked\n * as needing to be tokenized.\n */\nexport class TokenizationStateStore {\n\trequestTokens(startLineNumber: number, endLineNumberExclusive: number): void {\n\t\tfor (let lineNumber = startLineNumber; lineNumber < endLineNumberExclusive; lineNumber++) {\n\t\t\tthis._stateStore.markMustBeTokenized(lineNumber - 1);\n\t\t}\n\t}\n}\n",
"fileName": "./1.tst"
},
"modified": {
"content": "/*---------------------------------------------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Licensed under the MIT License. See License.txt in the project root for license information.\n *--------------------------------------------------------------------------------------------*/\n\nimport * as arrays from 'vs/base/common/arrays';\nimport { IdleDeadline, runWhenIdle } from 'vs/base/common/async';\nimport { BugIndicatingError, onUnexpectedError } from 'vs/base/common/errors';\nimport { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';\nimport { setTimeout0 } from 'vs/base/common/platform';\nimport { StopWatch } from 'vs/base/common/stopwatch';\nimport { countEOL } from 'vs/editor/common/core/eolCounter';\nimport { Position } from 'vs/editor/common/core/position';\nimport { IRange } from 'vs/editor/common/core/range';\nimport { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes';\nimport { EncodedTokenizationResult, IBackgroundTokenizationStore, IBackgroundTokenizer, ILanguageIdCodec, IState, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/languages';\nimport { nullTokenizeEncoded } from 'vs/editor/common/languages/nullTokenize';\nimport { ITextModel } from 'vs/editor/common/model';\nimport { TextModel } from 'vs/editor/common/model/textModel';\nimport { TokenizationTextModelPart } from 'vs/editor/common/model/tokenizationTextModelPart';\nimport { IModelContentChangedEvent, IModelLanguageChangedEvent } from 'vs/editor/common/textModelEvents';\nimport { ContiguousMultilineTokensBuilder } from 'vs/editor/common/tokens/contiguousMultilineTokensBuilder';\nimport { LineTokens } from 'vs/editor/common/tokens/lineTokens';\n\n/**\n * An array that avoids being sparse by always\n * filling up unused indices with a default value.\n */\nexport class ContiguousGrowingArray<T> {\n\n\tprivate _store: T[] = [];\n\n\tconstructor(\n\t\tprivate readonly _default: T\n\t) { }\n\n\tpublic get(index: number): T {\n\t\tif (index < this._store.length) {\n\t\t\treturn this._store[index];\n\t\t}\n\t\treturn this._default;\n\t}\n\n\tpublic set(index: number, value: T): void {\n\t\twhile (index >= this._store.length) {\n\t\t\tthis._store[this._store.length] = this._default;\n\t\t}\n\t\tthis._store[index] = value;\n\t}\n\n\t// TODO have `replace` instead of `delete` and `insert`\n\tpublic delete(deleteIndex: number, deleteCount: number): void {\n\t\tif (deleteCount === 0 || deleteIndex >= this._store.length) {\n\t\t\treturn;\n\t\t}\n\t\tthis._store.splice(deleteIndex, deleteCount);\n\t}\n\n\tpublic insert(insertIndex: number, insertCount: number): void {\n\t\tif (insertCount === 0 || insertIndex >= this._store.length) {\n\t\t\treturn;\n\t\t}\n\t\tconst arr: T[] = [];\n\t\tfor (let i = 0; i < insertCount; i++) {\n\t\t\tarr[i] = this._default;\n\t\t}\n\t\tthis._store = arrays.arrayInsert(this._store, insertIndex, arr);\n\t}\n}\n\nconst enum Constants {\n\tCHEAP_TOKENIZATION_LENGTH_LIMIT = 1024\n}\n\n/**\n * Stores the states at the start of each line and keeps track of which lines\n * must be re-tokenized. Also uses state equality to quickly validate lines\n * that don't need to be re-tokenized.\n *\n * For example, when typing on a line, the line gets marked as needing to be tokenized.\n * Once the line is tokenized, the end state is checked for equality against the begin\n * state of the next line. If the states are equal, tokenization doesn't need to run\n * again over the rest of the file. If the states are not equal, the next line gets marked\n * as needing to be tokenized.\n */\nexport class TokenizationStateStore {\n\trequestTokens(startLineNumber: number, endLineNumberExclusive: number): void {\n\t\tfor (let lineNumber = startLineNumber; lineNumber < endLineNumberExclusive; lineNumber++) {\n\t\t\tthis._stateStore.markMustBeTokenized(lineNumber - 1);\n\t\t}\n\t}\n}\n",
"fileName": "./2.tst"
},
"diffs": [
{
"originalRange": "[25,29)",
"modifiedRange": "[25,25)",
"innerChanges": null
},
{
"originalRange": "[75,75)",
"modifiedRange": "[71,75)",
"innerChanges": null
}
]
}

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

@ -2390,6 +2390,10 @@ declare namespace monaco.editor {
* A diff computation should throw if it takes longer than this value.
*/
maxComputationTimeMs: number;
/**
* If set, the diff computation should compute moves in addition to insertions and deletions.
*/
computeMoves: boolean;
}
/**
@ -2407,7 +2411,12 @@ declare namespace monaco.editor {
/**
* Maps all modified line ranges in the original to the corresponding line ranges in the modified text model.
*/
readonly changes: LineRangeMapping[];
readonly changes: readonly LineRangeMapping[];
/**
* Sorted by original line ranges.
* The original line ranges and the modified line ranges must be disjoint (but can be touching).
*/
readonly moves: readonly MovedText[];
}
/**
@ -2415,6 +2424,7 @@ declare namespace monaco.editor {
*/
export class LineRange {
static fromRange(range: Range): LineRange;
static subtract(a: LineRange, b: LineRange | undefined): LineRange[];
/**
* @param lineRanges An array of sorted line ranges.
*/
@ -2471,7 +2481,7 @@ declare namespace monaco.editor {
* Maps a line range in the original text model to a line range in the modified text model.
*/
export class LineRangeMapping {
static inverse(mapping: LineRangeMapping[], originalLineCount: number, modifiedLineCount: number): LineRangeMapping[];
static inverse(mapping: readonly LineRangeMapping[], originalLineCount: number, modifiedLineCount: number): LineRangeMapping[];
/**
* The line range in the original text model.
*/
@ -2507,6 +2517,24 @@ declare namespace monaco.editor {
constructor(originalRange: Range, modifiedRange: Range);
toString(): string;
}
export class MovedText {
readonly lineRangeMapping: SimpleLineRangeMapping;
/**
* The diff from the original text to the moved text.
* Must be contained in the original/modified line range.
* Can be empty if the text didn't change (only moved).
*/
readonly changes: readonly LineRangeMapping[];
constructor(lineRangeMapping: SimpleLineRangeMapping, changes: readonly LineRangeMapping[]);
}
export class SimpleLineRangeMapping {
readonly originalRange: LineRange;
readonly modifiedRange: LineRange;
constructor(originalRange: LineRange, modifiedRange: LineRange);
toString(): string;
}
export interface IDimension {
width: number;
height: number;
@ -3911,6 +3939,10 @@ declare namespace monaco.editor {
* Defaults to false.
*/
collapseUnchangedRegions?: boolean;
/**
* Defaults to false.
*/
showMoves?: boolean;
};
}

View file

@ -514,7 +514,7 @@ export class InteractiveEditorController implements IEditorContribution {
const textModelNplus1 = this._modelService.createModel(createTextBufferFactoryFromSnapshot(this._activeSession.textModelN.createSnapshot()), null, undefined, true);
textModelNplus1.applyEdits(editOperations);
const diff = await this._editorWorkerService.computeDiff(this._activeSession.textModel0.uri, textModelNplus1.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: 5000 }, 'advanced');
const diff = await this._editorWorkerService.computeDiff(this._activeSession.textModel0.uri, textModelNplus1.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: 5000, computeMoves: false }, 'advanced');
this._activeSession.lastTextModelChanges = diff?.changes ?? [];
textModelNplus1.dispose();

View file

@ -150,7 +150,7 @@ export class InteractiveEditorLivePreviewWidget extends ZoneWidget {
this._isVisible = true;
}
private _updateFromChanges(range: Range, changes: LineRangeMapping[]): void {
private _updateFromChanges(range: Range, changes: readonly LineRangeMapping[]): void {
assertType(this.editor.hasModel());
if (changes.length === 0 || this._session.textModel0.getValueLength() === 0) {
@ -174,7 +174,7 @@ export class InteractiveEditorLivePreviewWidget extends ZoneWidget {
// --- inline diff
private _renderChangesWithInlineDiff(changes: LineRangeMapping[]) {
private _renderChangesWithInlineDiff(changes: readonly LineRangeMapping[]) {
const original = this._session.textModel0;
const decorations: IModelDeltaDecoration[] = [];
@ -221,7 +221,7 @@ export class InteractiveEditorLivePreviewWidget extends ZoneWidget {
// --- full diff
private _renderChangesWithFullDiff(changes: LineRangeMapping[], range: Range) {
private _renderChangesWithFullDiff(changes: readonly LineRangeMapping[], range: Range) {
const modified = this.editor.getModel()!;
const ranges = this._computeHiddenRanges(modified, range, changes);
@ -250,7 +250,7 @@ export class InteractiveEditorLivePreviewWidget extends ZoneWidget {
super.hide();
}
private _computeHiddenRanges(model: ITextModel, range: Range, changes: LineRangeMapping[]) {
private _computeHiddenRanges(model: ITextModel, range: Range, changes: readonly LineRangeMapping[]) {
assertType(changes.length > 0);
let originalLineRange = changes[0].originalRange;

View file

@ -110,7 +110,7 @@ export class Session {
private _lastInput: string | undefined;
private _lastExpansionState: ExpansionState | undefined;
private _lastTextModelChanges: LineRangeMapping[] | undefined;
private _lastTextModelChanges: readonly LineRangeMapping[] | undefined;
private _isUnstashed: boolean = false;
private readonly _exchange: SessionExchange[] = [];
private readonly _startTime = new Date();
@ -185,7 +185,7 @@ export class Session {
return this._lastTextModelChanges ?? [];
}
set lastTextModelChanges(changes: LineRangeMapping[]) {
set lastTextModelChanges(changes: readonly LineRangeMapping[]) {
this._lastTextModelChanges = changes;
}

View file

@ -554,7 +554,7 @@ export class InteractiveEditorWidget {
// --- preview
showEditsPreview(textModelv0: ITextModel, edits: ISingleEditOperation[], changes: LineRangeMapping[]) {
showEditsPreview(textModelv0: ITextModel, edits: ISingleEditOperation[], changes: readonly LineRangeMapping[]) {
if (changes.length === 0) {
this.hideEditsPreview();
return;

View file

@ -41,6 +41,7 @@ export class MergeDiffComputer implements IMergeDiffComputer {
{
ignoreTrimWhitespace: false,
maxComputationTimeMs: 0,
computeMoves: false,
},
diffAlgorithm,
);

View file

@ -286,7 +286,7 @@ class MergeModelInterface extends Disposable {
const result = await linesDiffComputers.legacy.computeDiff(
textModel1.getLinesContent(),
textModel2.getLinesContent(),
{ ignoreTrimWhitespace: false, maxComputationTimeMs: 10000 }
{ ignoreTrimWhitespace: false, maxComputationTimeMs: 10000, computeMoves: false }
);
const changes = result.changes.map(c =>
new DetailedLineRangeMapping(