First draft of diff editor v2.

This commit is contained in:
Henning Dieterichs 2023-05-26 15:19:59 +02:00
parent d939e5d08f
commit 397f2e7c34
No known key found for this signature in database
GPG key ID: 771381EFFDB9EC06
29 changed files with 2169 additions and 43 deletions

View file

@ -51,6 +51,8 @@
"--vscode-commandCenter-foreground",
"--vscode-commandCenter-inactiveBorder",
"--vscode-commandCenter-inactiveForeground",
"--vscode-commentsView-resolvedIcon",
"--vscode-commentsView-unresolvedIcon",
"--vscode-contrastActiveBorder",
"--vscode-contrastBorder",
"--vscode-debugConsole-errorForeground",
@ -97,6 +99,7 @@
"--vscode-diffEditor-removedLineBackground",
"--vscode-diffEditor-removedTextBackground",
"--vscode-diffEditor-removedTextBorder",
"--vscode-diffEditor-unchangedRegionBackground",
"--vscode-diffEditorGutter-insertedLineBackground",
"--vscode-diffEditorGutter-removedLineBackground",
"--vscode-diffEditorOverview-insertedForeground",
@ -233,6 +236,8 @@
"--vscode-editorOverviewRuler-background",
"--vscode-editorOverviewRuler-border",
"--vscode-editorOverviewRuler-bracketMatchForeground",
"--vscode-editorOverviewRuler-commentForeground",
"--vscode-editorOverviewRuler-commentUnresolvedForeground",
"--vscode-editorOverviewRuler-commonContentForeground",
"--vscode-editorOverviewRuler-currentContentForeground",
"--vscode-editorOverviewRuler-deletedForeground",
@ -309,6 +314,8 @@
"--vscode-interactiveEditor-border",
"--vscode-interactiveEditor-regionHighlight",
"--vscode-interactiveEditor-shadow",
"--vscode-interactiveEditorDiff-inserted",
"--vscode-interactiveEditorDiff-removed",
"--vscode-interactiveEditorInput-background",
"--vscode-interactiveEditorInput-border",
"--vscode-interactiveEditorInput-focusBorder",
@ -457,7 +464,6 @@
"--vscode-problemsErrorIcon-foreground",
"--vscode-problemsInfoIcon-foreground",
"--vscode-problemsWarningIcon-foreground",
"--vscode-problemsSuccessIcon-foreground",
"--vscode-profileBadge-background",
"--vscode-profileBadge-foreground",
"--vscode-progressBar-background",
@ -520,6 +526,8 @@
"--vscode-statusBar-noFolderBackground",
"--vscode-statusBar-noFolderBorder",
"--vscode-statusBar-noFolderForeground",
"--vscode-statusBar-offlineBackground",
"--vscode-statusBar-offlineForeground",
"--vscode-statusBarItem-activeBackground",
"--vscode-statusBarItem-compactHoverBackground",
"--vscode-statusBarItem-errorBackground",
@ -733,4 +741,4 @@
"--z-index-run-button-container",
"--zoom-factor"
]
}
}

View file

@ -1865,7 +1865,7 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } & Partia
el.appendChild(c);
} else if (typeof c === 'string') {
el.append(c);
} else {
} else if ('root' in c) {
Object.assign(result, c);
el.appendChild(c.root);
}

View file

@ -1216,6 +1216,16 @@ export interface IDiffEditor extends editorCommon.IEditor {
* @internal
*/
setBoundarySashes(sashes: IBoundarySashes): void;
/**
* @internal
*/
goToDiff(target: 'next' | 'previous'): void;
/**
* @internal
*/
revealFirstDiff(): unknown;
}
/**

View file

@ -60,6 +60,7 @@ import { getThemeTypeSelector, IColorTheme, IThemeService, registerThemingPartic
import { ThemeIcon } from 'vs/base/common/themables';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { DiffNavigator } from 'vs/editor/browser/widget/diffNavigator';
export interface IDiffCodeEditorWidgetOptions {
originalEditor?: ICodeEditorWidgetOptions;
@ -242,6 +243,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE
private isEmbeddedDiffEditorKey: IContextKey<boolean>;
private _diffNavigator: DiffNavigator | undefined;
constructor(
domElement: HTMLElement,
options: Readonly<editorBrowser.IDiffEditorConstructionOptions>,
@ -289,7 +292,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE
renderOverviewRuler: true,
diffWordWrap: 'inherit',
diffAlgorithm: 'advanced',
accessibilityVerbose: false
accessibilityVerbose: false,
experimental: {
collapseUnchangedRegions: false,
},
});
this.isEmbeddedDiffEditorKey = EditorContextKeys.isEmbeddedDiffEditor.bindTo(this._contextKeyService);
@ -860,6 +866,12 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE
this._layoutOverviewViewport();
this._onDidChangeModel.fire();
// Diff navigator
this._diffNavigator = this._register(this._instantiationService.createInstance(DiffNavigator, this, {
alwaysRevealFirst: false,
findResultLoop: this.getModifiedEditor().getOption(EditorOption.find).loop
}));
}
public getContainerDomNode(): HTMLElement {
@ -1536,6 +1548,21 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE
equivalentLineNumber: this._getEquivalentLineForModifiedLineNumber(lineNumber)
};
}
public goToDiff(target: 'previous' | 'next'): void {
if (target === 'next') {
this._diffNavigator?.next();
} else {
this._diffNavigator?.previous();
}
}
public revealFirstDiff(): void {
// This is a hack, but it works.
if (this._diffNavigator) {
this._diffNavigator.revealFirst = true;
}
}
}
interface IDataSource {
@ -2785,6 +2812,9 @@ function validateDiffEditorOptions(options: Readonly<IDiffEditorOptions>, defaul
diffWordWrap: validateDiffWordWrap(options.diffWordWrap, defaults.diffWordWrap),
diffAlgorithm: validateStringSetOption(options.diffAlgorithm, defaults.diffAlgorithm, ['legacy', 'advanced'], { 'smart': 'legacy', 'experimental': 'advanced' }),
accessibilityVerbose: validateBooleanOption(options.accessibilityVerbose, defaults.accessibilityVerbose),
experimental: {
collapseUnchangedRegions: false,
},
};
}

View file

@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
export const diffFullLineAddDecoration = ModelDecorationOptions.register({
className: 'line-insert',
description: 'line-insert',
isWholeLine: true,
});
export const diffFullLineDeleteDecoration = ModelDecorationOptions.register({
className: 'line-delete',
description: 'line-delete',
isWholeLine: true,
});
export const diffAddDecoration = ModelDecorationOptions.register({
className: 'char-insert',
description: 'char-insert',
});
export const diffDeleteDecoration = ModelDecorationOptions.register({
className: 'char-delete',
description: 'char-delete',
});

View file

@ -0,0 +1,170 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IDimension } from 'vs/editor/common/core/dimension';
import { IPosition, Position } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { ISelection, Selection } from 'vs/editor/common/core/selection';
import { IEditor, IEditorAction, IEditorDecorationsCollection, IEditorModel, IEditorViewState, ScrollType } from 'vs/editor/common/editorCommon';
import { IModelDecorationsChangeAccessor, IModelDeltaDecoration } from 'vs/editor/common/model';
export abstract class DelegatingEditor extends Disposable implements IEditor {
private static idCounter = 0;
private readonly _id = ++DelegatingEditor.idCounter;
private readonly _onDidDispose = this._register(new Emitter<void>());
public readonly onDidDispose = this._onDidDispose.event;
protected abstract get _targetEditor(): CodeEditorWidget;
getId(): string { return this.getEditorType() + ':' + this._id; }
abstract getEditorType(): string;
abstract updateOptions(newOptions: IEditorOptions): void;
abstract onVisible(): void;
abstract onHide(): void;
abstract layout(dimension?: IDimension | undefined): void;
abstract hasTextFocus(): boolean;
abstract saveViewState(): IEditorViewState | null;
abstract restoreViewState(state: IEditorViewState | null): void;
abstract getModel(): IEditorModel | null;
abstract setModel(model: IEditorModel | null): void;
// #region editorBrowser.IDiffEditor: Delegating to modified Editor
public getVisibleColumnFromPosition(position: IPosition): number {
return this._targetEditor.getVisibleColumnFromPosition(position);
}
public getStatusbarColumn(position: IPosition): number {
return this._targetEditor.getStatusbarColumn(position);
}
public getPosition(): Position | null {
return this._targetEditor.getPosition();
}
public setPosition(position: IPosition, source: string = 'api'): void {
this._targetEditor.setPosition(position, source);
}
public revealLine(lineNumber: number, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealLine(lineNumber, scrollType);
}
public revealLineInCenter(lineNumber: number, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealLineInCenter(lineNumber, scrollType);
}
public revealLineInCenterIfOutsideViewport(lineNumber: number, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealLineInCenterIfOutsideViewport(lineNumber, scrollType);
}
public revealLineNearTop(lineNumber: number, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealLineNearTop(lineNumber, scrollType);
}
public revealPosition(position: IPosition, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealPosition(position, scrollType);
}
public revealPositionInCenter(position: IPosition, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealPositionInCenter(position, scrollType);
}
public revealPositionInCenterIfOutsideViewport(position: IPosition, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealPositionInCenterIfOutsideViewport(position, scrollType);
}
public revealPositionNearTop(position: IPosition, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealPositionNearTop(position, scrollType);
}
public getSelection(): Selection | null {
return this._targetEditor.getSelection();
}
public getSelections(): Selection[] | null {
return this._targetEditor.getSelections();
}
public setSelection(range: IRange, source?: string): void;
public setSelection(editorRange: Range, source?: string): void;
public setSelection(selection: ISelection, source?: string): void;
public setSelection(editorSelection: Selection, source?: string): void;
public setSelection(something: any, source: string = 'api'): void {
this._targetEditor.setSelection(something, source);
}
public setSelections(ranges: readonly ISelection[], source: string = 'api'): void {
this._targetEditor.setSelections(ranges, source);
}
public revealLines(startLineNumber: number, endLineNumber: number, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealLines(startLineNumber, endLineNumber, scrollType);
}
public revealLinesInCenter(startLineNumber: number, endLineNumber: number, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealLinesInCenter(startLineNumber, endLineNumber, scrollType);
}
public revealLinesInCenterIfOutsideViewport(startLineNumber: number, endLineNumber: number, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealLinesInCenterIfOutsideViewport(startLineNumber, endLineNumber, scrollType);
}
public revealLinesNearTop(startLineNumber: number, endLineNumber: number, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealLinesNearTop(startLineNumber, endLineNumber, scrollType);
}
public revealRange(range: IRange, scrollType: ScrollType = ScrollType.Smooth, revealVerticalInCenter: boolean = false, revealHorizontal: boolean = true): void {
this._targetEditor.revealRange(range, scrollType, revealVerticalInCenter, revealHorizontal);
}
public revealRangeInCenter(range: IRange, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealRangeInCenter(range, scrollType);
}
public revealRangeInCenterIfOutsideViewport(range: IRange, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealRangeInCenterIfOutsideViewport(range, scrollType);
}
public revealRangeNearTop(range: IRange, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealRangeNearTop(range, scrollType);
}
public revealRangeNearTopIfOutsideViewport(range: IRange, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealRangeNearTopIfOutsideViewport(range, scrollType);
}
public revealRangeAtTop(range: IRange, scrollType: ScrollType = ScrollType.Smooth): void {
this._targetEditor.revealRangeAtTop(range, scrollType);
}
public getSupportedActions(): IEditorAction[] {
return this._targetEditor.getSupportedActions();
}
public focus(): void {
this._targetEditor.focus();
}
public trigger(source: string | null | undefined, handlerId: string, payload: any): void {
this._targetEditor.trigger(source, handlerId, payload);
}
public createDecorationsCollection(decorations?: IModelDeltaDecoration[]): IEditorDecorationsCollection {
return this._targetEditor.createDecorationsCollection(decorations);
}
public changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => any): any {
return this._targetEditor.changeDecorations(callback);
}
// #endregion
}

View file

@ -0,0 +1,74 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Sash, Orientation, ISashEvent, IBoundarySashes, SashState } from 'vs/base/browser/ui/sash/sash';
import { Disposable } from 'vs/base/common/lifecycle';
import { IObservable, IReader, autorun, derived, observableValue } from 'vs/base/common/observable';
export class DiffEditorSash extends Disposable {
private readonly _sashRatio = observableValue<number | undefined>('sashRatio', undefined);
public readonly sashLeft = derived('sashLeft', reader => {
const ratio = this._sashRatio.read(reader) ?? this._defaultSashRatio.read(reader);
return this._computeSashLeft(ratio, reader);
});
private readonly _sash = this._register(new Sash(this._domNode, {
getVerticalSashTop: (_sash: Sash): number => 0,
getVerticalSashLeft: (_sash: Sash): number => this.sashLeft.get(),
getVerticalSashHeight: (_sash: Sash): number => this._dimensions.height.get(),
}, { orientation: Orientation.VERTICAL }));
private _startSashPosition: number | undefined = undefined;
constructor(
private readonly _enableSplitViewResizing: IObservable<boolean>,
private readonly _defaultSashRatio: IObservable<number>,
private readonly _domNode: HTMLElement,
private readonly _dimensions: { height: IObservable<number>; width: IObservable<number> },
) {
super();
this._register(this._sash.onDidStart(() => {
this._startSashPosition = this.sashLeft.get();
}));
this._register(this._sash.onDidChange((e: ISashEvent) => {
const contentWidth = this._dimensions.width.get();
const sashPosition = this._computeSashLeft((this._startSashPosition! + (e.currentX - e.startX)) / contentWidth, undefined);
this._sashRatio.set(sashPosition / contentWidth, undefined);
}));
this._register(this._sash.onDidEnd(() => this._sash.layout()));
this._register(this._sash.onDidReset(() => this._sashRatio.set(undefined, undefined)));
this._register(autorun('update sash layout', (reader) => {
const enabled = this._enableSplitViewResizing.read(reader);
this._sash.state = enabled ? SashState.Enabled : SashState.Disabled;
this.sashLeft.read(reader);
this._sash.layout();
}));
}
setBoundarySashes(sashes: IBoundarySashes): void {
this._sash.orthogonalEndSash = sashes.bottom;
}
private _computeSashLeft(desiredRatio: number, reader: IReader | undefined): number {
const contentWidth = this._dimensions.width.read(reader);
const midPoint = Math.floor(this._defaultSashRatio.read(reader) * contentWidth);
const sashLeft = this._enableSplitViewResizing.read(reader) ? Math.floor(desiredRatio * contentWidth) : midPoint;
const MINIMUM_EDITOR_WIDTH = 100;
if (contentWidth <= MINIMUM_EDITOR_WIDTH * 2) {
return midPoint;
}
if (sashLeft < MINIMUM_EDITOR_WIDTH) {
return MINIMUM_EDITOR_WIDTH;
}
if (sashLeft > contentWidth - MINIMUM_EDITOR_WIDTH) {
return contentWidth - MINIMUM_EDITOR_WIDTH;
}
return sashLeft;
}
}

View file

@ -0,0 +1,57 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Codicon } from 'vs/base/common/codicons';
import { ThemeIcon } from 'vs/base/common/themables';
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
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';
export class ToggleCollapseUnchangedRegions extends Action2 {
constructor() {
super({
id: 'diffEditor.toggleCollapseUnchangedRegions',
title: { value: localize('toggleCollapseUnchangedRegions', "Toggle Collapse Unchanged Regions"), original: 'Toggle Collapse Unchanged Regions' },
icon: Codicon.map,
precondition: ContextKeyEqualsExpr.create('diffEditorVersion', 2),
});
}
run(accessor: ServicesAccessor, ...args: unknown[]): void {
const configurationService = accessor.get(IConfigurationService);
const newValue = !configurationService.getValue<boolean>('diffEditor.experimental.collapseUnchangedRegions');
configurationService.updateValue('diffEditor.experimental.collapseUnchangedRegions', newValue);
}
}
registerAction2(ToggleCollapseUnchangedRegions);
MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
command: {
id: new ToggleCollapseUnchangedRegions().desc.id,
title: localize('collapseUnchangedRegions', "Collapse Unchanged Regions"),
icon: Codicon.map
},
group: 'navigation',
when: ContextKeyExpr.and(
ContextKeyExpr.has('config.diffEditor.experimental.collapseUnchangedRegions'),
ContextKeyEqualsExpr.create('diffEditorVersion', 2)
)
});
MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
command: {
id: new ToggleCollapseUnchangedRegions().desc.id,
title: localize('showUnchangedRegions', "Show Unchanged Regions"),
icon: ThemeIcon.modify(Codicon.map, 'disabled'),
},
group: 'navigation',
when: ContextKeyExpr.and(
ContextKeyExpr.has('config.diffEditor.experimental.collapseUnchangedRegions').negate(),
ContextKeyEqualsExpr.create('diffEditorVersion', 2)
)
});

View file

@ -0,0 +1,499 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { h } from 'vs/base/browser/dom';
import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash';
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 { Constants } from 'vs/base/common/uint';
import 'vs/css!./style';
import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration';
import { ICodeEditor, IDiffEditor, IDiffEditorConstructionOptions, IDiffLineInformation } from 'vs/editor/browser/editorBrowser';
import { EditorExtensionsRegistry, IDiffEditorContributionDescription } from 'vs/editor/browser/editorExtensions';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget';
import { IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEditorWidget';
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 { 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 { 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';
import { localize } from 'vs/nls';
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';
const diffEditorDefaultOptions: ValidDiffEditorBaseOptions = {
enableSplitViewResizing: true,
splitViewDefaultRatio: 0.5,
renderSideBySide: true,
renderMarginRevertIcon: true,
maxComputationTime: 5000,
maxFileSize: 50,
ignoreTrimWhitespace: true,
renderIndicators: true,
originalEditable: false,
diffCodeLens: false,
renderOverviewRuler: true,
diffWordWrap: 'inherit',
diffAlgorithm: 'advanced',
accessibilityVerbose: false,
experimental: {
collapseUnchangedRegions: false,
}
};
export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
private readonly elements = h('div.monaco-diff-editor.side-by-side', { style: { position: 'relative', height: '100%' } }, [
h('div.editor.original@original', { style: { position: 'absolute', height: '100%' } }),
h('div.editor.modified@modified', { style: { position: 'absolute', height: '100%' } }),
]);
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 _onDidContentSizeChange = this._register(new Emitter<IContentSizeChangedEvent>());
public readonly onDidContentSizeChange = this._onDidContentSizeChange.event;
private readonly _modifiedEditor: CodeEditorWidget;
private readonly _originalEditor: CodeEditorWidget;
private readonly _contextKeyService = this._register(this._parentContextKeyService.createScoped(this._domElement));
private readonly _instantiationService = this._parentInstantiationService.createChild(
new ServiceCollection([IContextKeyService, this._contextKeyService])
);
private readonly _rootSizeObserver: ObservableElementSizeObserver;
private readonly _options: ISettableObservable<ValidDiffEditorBaseOptions>;
private _isHandlingScrollEvent = false;
private readonly _sash: DiffEditorSash;
private readonly _renderOverviewRuler: IObservable<boolean>;
constructor(
private readonly _domElement: HTMLElement,
options: Readonly<IDiffEditorConstructionOptions>,
codeEditorWidgetOptions: IDiffCodeEditorWidgetOptions,
@IContextKeyService private readonly _parentContextKeyService: IContextKeyService,
@IInstantiationService private readonly _parentInstantiationService: IInstantiationService,
@ICodeEditorService codeEditorService: ICodeEditorService,
) {
super();
codeEditorService.willCreateDiffEditor();
this._contextKeyService.createKey('isInDiffEditor', true);
this._contextKeyService.createKey('diffEditorVersion', 2);
this._contextKeyService.createKey('isInEmbeddedDiffEditor',
typeof options.isInEmbeddedEditor !== 'undefined' ? options.isInEmbeddedEditor : false
);
this._options = observableValue<ValidDiffEditorBaseOptions>('options', validateDiffEditorOptions(options || {}, diffEditorDefaultOptions));
this._domElement.appendChild(this.elements.root);
this._rootSizeObserver = this._register(new ObservableElementSizeObserver(this.elements.root, options.dimension));
this._rootSizeObserver.setAutomaticLayout(options.automaticLayout ?? false);
this._originalEditor = this._createLeftHandSideEditor(options, codeEditorWidgetOptions.originalEditor || {});
this._modifiedEditor = this._createRightHandSideEditor(options, codeEditorWidgetOptions.modifiedEditor || {});
this._register(applyObservableDecorations(this._originalEditor, this._decorations.map(d => d?.originalDecorations || [])));
this._register(applyObservableDecorations(this._modifiedEditor, this._decorations.map(d => d?.modifiedDecorations || [])));
this._renderOverviewRuler = this._options.map(o => o.renderOverviewRuler);
this._sash = this._register(new DiffEditorSash(
this._options.map(o => o.enableSplitViewResizing),
this._options.map(o => o.splitViewDefaultRatio),
this.elements.root,
{
height: this._rootSizeObserver.height,
width: this._rootSizeObserver.width.map((w, reader) => w - (this._renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)),
}
));
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,
this.elements.root,
this._diffModel,
this._rootSizeObserver.width,
this._rootSizeObserver.height,
this._layoutInfo.map(i => i.modifiedEditor),
this._renderOverviewRuler,
));
this._createDiffEditorContributions();
codeEditorService.addDiffEditor(this);
this._register(keepAlive(this._layoutInfo, true));
}
private readonly _layoutInfo = derived('modifiedEditorLayoutInfo', (reader) => {
const width = this._rootSizeObserver.width.read(reader);
const height = this._rootSizeObserver.height.read(reader);
const sashLeft = this._sash.sashLeft.read(reader);
this.elements.original.style.width = sashLeft + 'px';
this.elements.original.style.left = '0px';
this.elements.modified.style.width = (width - sashLeft) + 'px';
this.elements.modified.style.left = sashLeft + 'px';
this._originalEditor.layout({ width: sashLeft, height: height });
this._modifiedEditor.layout({
width: width - sashLeft -
(this._renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0),
height
});
return { modifiedEditor: this._modifiedEditor.getLayoutInfo() };
});
private readonly _decorations = derived('decorations', (reader) => {
const diff = this._diffModel.read(reader)?.diff.read(reader);
if (!diff) {
return null;
}
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 i of c.innerChanges || []) {
originalDecorations.push({ range: i.originalRange, options: diffDeleteDecoration });
modifiedDecorations.push({ range: i.modifiedRange, options: diffAddDecoration });
}
}
return { originalDecorations, modifiedDecorations };
});
private _createDiffEditorContributions() {
const contributions: IDiffEditorContributionDescription[] = EditorExtensionsRegistry.getDiffEditorContributions();
for (const desc of contributions) {
try {
this._register(this._instantiationService.createInstance(desc.ctor, this));
} catch (err) {
onUnexpectedError(err);
}
}
}
private _createLeftHandSideEditor(options: Readonly<IDiffEditorConstructionOptions>, codeEditorWidgetOptions: ICodeEditorWidgetOptions): CodeEditorWidget {
const editor = this._createInnerEditor(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)));
return editor;
}
private _createRightHandSideEditor(options: Readonly<IDiffEditorConstructionOptions>, codeEditorWidgetOptions: ICodeEditorWidgetOptions): CodeEditorWidget {
const editor = this._createInnerEditor(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)));
// Revert change when an arrow is clicked.
/*TODO
this._register(editor.onMouseDown(event => {
if (!event.event.rightButton && event.target.position && event.target.element?.className.includes('arrow-revert-change')) {
const lineNumber = event.target.position.lineNumber;
const viewZone = event.target as editorBrowser.IMouseTargetViewZone | undefined;
const change = this._diffComputationResult?.changes.find(c =>
// delete change
viewZone?.detail.afterLineNumber === c.modifiedStartLineNumber ||
// other changes
(c.modifiedEndLineNumber > 0 && c.modifiedStartLineNumber === lineNumber));
if (change) {
this.revertChange(change);
}
event.event.stopPropagation();
this._updateDecorations();
return;
}
}));*/
return editor;
}
protected _createInnerEditor(instantiationService: IInstantiationService, container: HTMLElement, options: Readonly<IEditorConstructionOptions>, editorWidgetOptions: ICodeEditorWidgetOptions): CodeEditorWidget {
const editor = instantiationService.createInstance(CodeEditorWidget, container, options, editorWidgetOptions);
this._register(editor.onDidContentSizeChange(e => {
const width = this._originalEditor.getContentWidth() + this._modifiedEditor.getContentWidth() + OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH;
const height = Math.max(this._modifiedEditor.getContentHeight(), this._originalEditor.getContentHeight());
this._onDidContentSizeChange.fire({
contentHeight: height,
contentWidth: width,
contentHeightChanged: e.contentHeightChanged,
contentWidthChanged: e.contentWidthChanged
});
}));
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;
}
}));
return editor;
}
private _adjustOptionsForLeftHandSide(options: Readonly<IDiffEditorConstructionOptions>): IEditorConstructionOptions {
const result = this._adjustOptionsForSubEditor(options);
if (!options.renderSideBySide) {
// never wrap hidden editor
result.wordWrapOverride1 = 'off';
result.wordWrapOverride2 = 'off';
} else {
result.wordWrapOverride1 = this._options.get().diffWordWrap;
}
if (options.originalAriaLabel) {
result.ariaLabel = options.originalAriaLabel;
}
result.ariaLabel = this._updateAriaLabel(result.ariaLabel);
result.readOnly = !options.originalEditable;
result.dropIntoEditor = { enabled: !result.readOnly };
result.extraEditorClassName = 'original-in-monaco-diff-editor';
return result;
}
private _adjustOptionsForRightHandSide(options: Readonly<IDiffEditorConstructionOptions>): IEditorConstructionOptions {
const result = this._adjustOptionsForSubEditor(options);
if (options.modifiedAriaLabel) {
result.ariaLabel = options.modifiedAriaLabel;
}
result.ariaLabel = this._updateAriaLabel(result.ariaLabel);
result.wordWrapOverride1 = this._options.get().diffWordWrap;
result.revealHorizontalRightPadding = EditorOptions.revealHorizontalRightPadding.defaultValue + OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH;
result.scrollbar!.verticalHasArrows = false;
result.extraEditorClassName = 'modified-in-monaco-diff-editor';
return result;
}
private _adjustOptionsForSubEditor(options: Readonly<IDiffEditorConstructionOptions>): IEditorConstructionOptions {
const clonedOptions = {
...options,
dimension: {
height: 0,
width: 0
},
};
clonedOptions.inDiffEditor = true;
clonedOptions.automaticLayout = false;
// Clone scrollbar options before changing them
clonedOptions.scrollbar = { ...(clonedOptions.scrollbar || {}) };
clonedOptions.scrollbar.vertical = 'visible';
clonedOptions.folding = false;
clonedOptions.codeLens = this._options.get().diffCodeLens;
clonedOptions.fixedOverflowWidgets = true;
// clonedOptions.lineDecorationsWidth = '2ch';
// Clone minimap options before changing them
clonedOptions.minimap = { ...(clonedOptions.minimap || {}) };
clonedOptions.minimap.enabled = false;
return clonedOptions;
}
private _updateAriaLabel(ariaLabel: string | undefined): string | undefined {
const ariaNavigationTip = localize('diff-aria-navigation-tip', ' use Shift + F7 to navigate changes');
if (this._options.get().accessibilityVerbose) {
return ariaLabel + ariaNavigationTip;
} else if (ariaLabel) {
return ariaLabel.replaceAll(ariaNavigationTip, '');
}
return undefined;
}
protected override get _targetEditor(): CodeEditorWidget { return this._modifiedEditor; }
override getEditorType(): string { return EditorType.IDiffEditor; }
override onVisible(): void {
// TODO: Only compute diffs when diff editor is visible
this._originalEditor.onVisible();
this._modifiedEditor.onVisible();
}
override onHide(): void {
this._originalEditor.onHide();
this._modifiedEditor.onHide();
}
override layout(dimension?: IDimension | undefined): void {
this._rootSizeObserver.observe(dimension);
}
override hasTextFocus(): boolean {
return this._originalEditor.hasTextFocus() || this._modifiedEditor.hasTextFocus();
}
override saveViewState(): IDiffEditorViewState | null {
return null;
//throw new Error('Method not implemented.');
}
override restoreViewState(state: IDiffEditorViewState | null): void {
//throw new Error('Method not implemented.');
}
override getModel(): IDiffEditorModel | null { return this._model.get(); }
override setModel(model: IDiffEditorModel | null): void {
this._originalEditor.setModel(model ? model.original : null);
this._modifiedEditor.setModel(model ? model.modified : null);
this._model.set(model, undefined);
this._diffModel.set(model ? new DiffModel(
model,
this._options.map(o => o.ignoreTrimWhitespace),
this._options.map(o => o.maxComputationTime),
this._options.map(o => o.experimental.collapseUnchangedRegions!),
this._instantiationService.createInstance(WorkerBasedDocumentDiffProvider, this._options.get())
) : null, undefined);
}
override updateOptions(_newOptions: IDiffEditorOptions): void {
const newOptions = validateDiffEditorOptions(_newOptions, this._options.get());
this._options.set(newOptions, undefined);
this._modifiedEditor.updateOptions(this._adjustOptionsForRightHandSide(_newOptions));
this._originalEditor.updateOptions(this._adjustOptionsForLeftHandSide(_newOptions));
}
getContainerDomNode(): HTMLElement { return this._domElement; }
getOriginalEditor(): ICodeEditor { return this._originalEditor; }
getModifiedEditor(): ICodeEditor { return this._modifiedEditor; }
setBoundarySashes(sashes: IBoundarySashes): void {
this._sash.setBoundarySashes(sashes);
}
readonly onDidUpdateDiff: Event<void> = e => {
return { dispose: () => { } };
};
get ignoreTrimWhitespace(): boolean {
return this._options.get().ignoreTrimWhitespace;
}
get maxComputationTime(): number {
return this._options.get().maxComputationTime;
}
get renderSideBySide(): boolean {
return this._options.get().renderSideBySide;
}
getLineChanges(): ILineChange[] | null {
return null;
//throw new Error('Method not implemented.');
}
getDiffComputationResult(): IDiffComputationResult | null {
return null;
//throw new Error('Method not implemented.');
}
getDiffLineInformationForOriginal(lineNumber: number): IDiffLineInformation | null {
return null;
//throw new Error('Method not implemented.');
}
getDiffLineInformationForModified(lineNumber: number): IDiffLineInformation | null {
return null;
//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());
}
goToDiff(target: 'previous' | 'next'): void {
const diffs = this._diffModel.get()?.diff.get()?.changes;
if (!diffs || diffs.length === 0) {
return;
}
const curLineNumber = this._modifiedEditor.getPosition()!.lineNumber;
let diff: LineRangeMapping | undefined;
if (target === 'next') {
diff = diffs.find(d => d.modifiedRange.startLineNumber > curLineNumber) ?? diffs[0];
} else {
diff = findLast(diffs, d => d.modifiedRange.startLineNumber < curLineNumber) ?? diffs[diffs.length - 1];
}
this._goTo(diff);
}
revealFirstDiff(): void {
const diffModel = this._diffModel.get();
if (!diffModel) {
return;
}
// wait for the diff computation to finish
waitForState(diffModel.isDiffUpToDate, s => s).then(() => {
const diffs = diffModel.diff.get()?.changes;
if (!diffs || diffs.length === 0) {
return;
}
this._goTo(diffs[0]);
});
}
}
function validateDiffEditorOptions(options: Readonly<IDiffEditorOptions>, defaults: ValidDiffEditorBaseOptions): ValidDiffEditorBaseOptions {
return {
enableSplitViewResizing: validateBooleanOption(options.enableSplitViewResizing, defaults.enableSplitViewResizing),
splitViewDefaultRatio: clampedFloat(options.splitViewDefaultRatio, 0.5, 0.1, 0.9),
renderSideBySide: validateBooleanOption(options.renderSideBySide, defaults.renderSideBySide),
renderMarginRevertIcon: validateBooleanOption(options.renderMarginRevertIcon, defaults.renderMarginRevertIcon),
maxComputationTime: clampedInt(options.maxComputationTime, defaults.maxComputationTime, 0, Constants.MAX_SAFE_SMALL_INTEGER),
maxFileSize: clampedInt(options.maxFileSize, defaults.maxFileSize, 0, Constants.MAX_SAFE_SMALL_INTEGER),
ignoreTrimWhitespace: validateBooleanOption(options.ignoreTrimWhitespace, defaults.ignoreTrimWhitespace),
renderIndicators: validateBooleanOption(options.renderIndicators, defaults.renderIndicators),
originalEditable: validateBooleanOption(options.originalEditable, defaults.originalEditable),
diffCodeLens: validateBooleanOption(options.diffCodeLens, defaults.diffCodeLens),
renderOverviewRuler: validateBooleanOption(options.renderOverviewRuler, defaults.renderOverviewRuler),
diffWordWrap: validateStringSetOption<'off' | 'on' | 'inherit'>(options.diffWordWrap, defaults.diffWordWrap, ['off', 'on', 'inherit']),
diffAlgorithm: validateStringSetOption(options.diffAlgorithm, defaults.diffAlgorithm, ['legacy', 'advanced'], { 'smart': 'legacy', 'experimental': 'advanced' }),
accessibilityVerbose: validateBooleanOption(options.accessibilityVerbose, defaults.accessibilityVerbose),
experimental: {
collapseUnchangedRegions: validateBooleanOption(options.experimental?.collapseUnchangedRegions, defaults.experimental.collapseUnchangedRegions!),
},
};
}

View file

@ -0,0 +1,344 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RunOnceScheduler } from 'vs/base/common/async';
import { Disposable } from 'vs/base/common/lifecycle';
import { IObservable, IReader, ITransaction, derived, observableSignal, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable';
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 { lineRangeMappingFromRangeMappings } from 'vs/editor/common/diff/standardLinesDiffComputer';
import { IDiffEditorModel } from 'vs/editor/common/editorCommon';
import { ITextModel } from 'vs/editor/common/model';
import { TextEditInfo } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper';
import { combineTextEditInfos } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/combineTextEditInfos';
import { lengthAdd, lengthDiffNonNegative, lengthOfRange, lengthToPosition, lengthZero, positionToLength } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length';
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 _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 : []
);
constructor(
model: IDiffEditorModel,
ignoreTrimWhitespace: IObservable<boolean>,
maxComputationTimeMs: IObservable<number>,
private readonly hideUnchangedRegions: 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);
this._diff.set(
applyModifiedEdits(diff, textEdits, model.original, model.modified),
undefined
);
debouncer.schedule();
}));
this._register(model.original.onDidChangeContent((e) => {
const diff = this._diff.get();
if (!diff) {
return;
}
const textEdits = TextEditInfo.fromModelContentChanges(e.changes);
this._diff.set(
applyOriginalEdits(diff, textEdits, model.original, model.modified),
undefined
);
debouncer.schedule();
}));
const documentDiffProviderOptionChanged = observableSignalFromEvent('documentDiffProviderOptionChanged', documentDiffProvider.onDidChange);
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);
let originalTextEditInfos: TextEditInfo[] = [];
store.add(model.original.onDidChangeContent((e) => {
const edits = TextEditInfo.fromModelContentChanges(e.changes);
originalTextEditInfos = combineTextEditInfos(originalTextEditInfos, edits);
}));
let modifiedTextEditInfos: TextEditInfo[] = [];
store.add(model.modified.onDidChangeContent((e) => {
const edits = TextEditInfo.fromModelContentChanges(e.changes);
modifiedTextEditInfos = combineTextEditInfos(modifiedTextEditInfos, edits);
}));
let result = await documentDiffProvider.computeDiff(model.original, model.modified, {
ignoreTrimWhitespace: ignoreTrimWhitespaceVal,
maxComputationTimeMs: maxComputationTimeMsVal,
});
result = applyOriginalEdits(result, originalTextEditInfos, model.original, model.modified);
result = applyModifiedEdits(result, modifiedTextEditInfos, model.original, model.modified);
const newUnchangedRegions = UnchangedRegion.fromDiffs(result.changes, model.original.getLineCount(), model.modified.getLineCount());
// Transfer state from cur state
const lastUnchangedRegions = this._unchangedRegions.get();
const lastUnchangedRegionsOrigRanges = lastUnchangedRegions.originalDecorationIds
.map(id => model.original.getDecorationRange(id))
.filter(r => !!r)
.map(r => LineRange.fromRange(r!));
const lastUnchangedRegionsModRanges = lastUnchangedRegions.modifiedDecorationIds
.map(id => model.modified.getDecorationRange(id))
.filter(r => !!r)
.map(r => LineRange.fromRange(r!));
for (const r of newUnchangedRegions) {
for (let i = 0; i < lastUnchangedRegions.regions.length; i++) {
if (r.originalRange.intersectsStrict(lastUnchangedRegionsOrigRanges[i])
&& r.modifiedRange.intersectsStrict(lastUnchangedRegionsModRanges[i])) {
r.setState(
lastUnchangedRegions.regions[i].visibleLineCountTop.get(),
lastUnchangedRegions.regions[i].visibleLineCountBottom.get(),
undefined,
);
break;
}
}
}
const originalDecorationIds = model.original.deltaDecorations(
lastUnchangedRegions.originalDecorationIds,
newUnchangedRegions.map(r => ({ range: r.originalRange.toInclusiveRange()!, options: { description: 'unchanged' } }))
);
const modifiedDecorationIds = model.modified.deltaDecorations(
lastUnchangedRegions.modifiedDecorationIds,
newUnchangedRegions.map(r => ({ range: r.modifiedRange.toInclusiveRange()!, options: { description: 'unchanged' } }))
);
transaction(tx => {
this._diff.set(result, tx);
this._isDiffUpToDate.set(true, tx);
this._unchangedRegions.set(
{
regions: newUnchangedRegions,
originalDecorationIds,
modifiedDecorationIds
},
tx
);
});
}));
}
public revealModifiedLine(lineNumber: number, tx: ITransaction): void {
const unchangedRegions = this._unchangedRegions.get().regions;
for (const r of unchangedRegions) {
if (r.getHiddenModifiedRange(undefined).contains(lineNumber)) {
r.showAll(tx); // TODO only unhide what is needed
return;
}
}
}
public revealOriginalLine(lineNumber: number, tx: ITransaction): void {
const unchangedRegions = this._unchangedRegions.get().regions;
for (const r of unchangedRegions) {
if (r.getHiddenOriginalRange(undefined).contains(lineNumber)) {
r.showAll(tx); // TODO only unhide what is needed
return;
}
}
}
}
export class UnchangedRegion {
public static fromDiffs(changes: LineRangeMapping[], originalLineCount: number, modifiedLineCount: number): UnchangedRegion[] {
const inversedMappings = LineRangeMapping.inverse(changes, originalLineCount, modifiedLineCount);
const result: UnchangedRegion[] = [];
const minHiddenLineCount = 3;
const minContext = 3;
for (const mapping of inversedMappings) {
let origStart = mapping.originalRange.startLineNumber;
let modStart = mapping.modifiedRange.startLineNumber;
let length = mapping.originalRange.length;
if (origStart === 1 && length > minContext + minHiddenLineCount) {
length -= minContext;
result.push(new UnchangedRegion(origStart, modStart, length, 0, 0));
} else if (origStart + length === originalLineCount + 1 && length > minContext + minHiddenLineCount) {
origStart += minContext;
modStart += minContext;
length -= minContext;
result.push(new UnchangedRegion(origStart, modStart, length, 0, 0));
} else if (length > minContext * 2 + minHiddenLineCount) {
origStart += minContext;
modStart += minContext;
length -= minContext * 2;
result.push(new UnchangedRegion(origStart, modStart, length, 0, 0));
}
}
return result;
}
public get originalRange(): LineRange {
return LineRange.ofLength(this.originalLineNumber, this.lineCount);
}
public get modifiedRange(): LineRange {
return LineRange.ofLength(this.modifiedLineNumber, this.lineCount);
}
private readonly _visibleLineCountTop = observableValue<number>('visibleLineCountTop', 0);
public readonly visibleLineCountTop: IObservable<number> = this._visibleLineCountTop;
private readonly _visibleLineCountBottom = observableValue<number>('visibleLineCountBottom', 0);
public readonly visibleLineCountBottom: IObservable<number> = this._visibleLineCountBottom;
constructor(
public readonly originalLineNumber: number,
public readonly modifiedLineNumber: number,
public readonly lineCount: number,
visibleLineCountTop: number,
visibleLineCountBottom: number,
) {
this._visibleLineCountTop.set(visibleLineCountTop, undefined);
this._visibleLineCountBottom.set(visibleLineCountBottom, undefined);
}
public getHiddenOriginalRange(reader: IReader | undefined): LineRange {
return LineRange.ofLength(
this.originalLineNumber + this._visibleLineCountTop.read(reader),
this.lineCount - this._visibleLineCountTop.read(reader) - this._visibleLineCountBottom.read(reader),
);
}
public getHiddenModifiedRange(reader: IReader | undefined): LineRange {
return LineRange.ofLength(
this.modifiedLineNumber + this._visibleLineCountTop.read(reader),
this.lineCount - this._visibleLineCountTop.read(reader) - this._visibleLineCountBottom.read(reader),
);
}
public showMoreAbove(tx: ITransaction | undefined): void {
const maxVisibleLineCountTop = this.lineCount - this._visibleLineCountBottom.get();
this._visibleLineCountTop.set(Math.min(this._visibleLineCountTop.get() + 10, maxVisibleLineCountTop), tx);
}
public showMoreBelow(tx: ITransaction | undefined): void {
const maxVisibleLineCountBottom = this.lineCount - this._visibleLineCountTop.get();
this._visibleLineCountBottom.set(Math.min(this._visibleLineCountBottom.get() + 10, maxVisibleLineCountBottom), tx);
}
public showAll(tx: ITransaction | undefined): void {
this._visibleLineCountBottom.set(this.lineCount - this._visibleLineCountTop.get(), tx);
}
public setState(visibleLineCountTop: number, visibleLineCountBottom: number, tx: ITransaction | undefined): void {
visibleLineCountTop = Math.min(visibleLineCountTop, this.lineCount);
visibleLineCountBottom = Math.min(visibleLineCountBottom, this.lineCount - visibleLineCountTop);
this._visibleLineCountTop.set(visibleLineCountTop, tx);
this._visibleLineCountBottom.set(visibleLineCountBottom, tx);
}
}
function applyOriginalEdits(diff: IDocumentDiff, textEdits: TextEditInfo[], originalTextModel: ITextModel, modifiedTextModel: ITextModel): IDocumentDiff {
if (textEdits.length === 0) {
return diff;
}
const diffTextEdits = diff.changes.flatMap(c => c.innerChanges!.map(c => new TextEditInfo(
positionToLength(c.modifiedRange.getStartPosition()),
positionToLength(c.modifiedRange.getEndPosition()),
lengthOfRange(c.originalRange).toLength(),
)));
const combined = combineTextEditInfos(diffTextEdits, textEdits);
let lastModifiedEndOffset = lengthZero;
let lastOriginalEndOffset = lengthZero;
const rangeMappings = combined.map(c => {
const originalStartOffset = lengthAdd(lastOriginalEndOffset, lengthDiffNonNegative(lastModifiedEndOffset, c.startOffset));
lastModifiedEndOffset = c.endOffset;
lastOriginalEndOffset = lengthAdd(originalStartOffset, c.newLength);
return new RangeMapping(
Range.fromPositions(lengthToPosition(originalStartOffset), lengthToPosition(lastOriginalEndOffset)),
Range.fromPositions(lengthToPosition(c.startOffset), lengthToPosition(c.endOffset)),
);
});
const changes = lineRangeMappingFromRangeMappings(
rangeMappings,
originalTextModel.getLinesContent(),
modifiedTextModel.getLinesContent(),
);
return {
identical: false,
quitEarly: false,
changes,
};
}
function applyModifiedEdits(diff: IDocumentDiff, textEdits: TextEditInfo[], originalTextModel: ITextModel, modifiedTextModel: ITextModel): IDocumentDiff {
if (textEdits.length === 0) {
return diff;
}
const diffTextEdits = diff.changes.flatMap(c => c.innerChanges!.map(c => new TextEditInfo(
positionToLength(c.originalRange.getStartPosition()),
positionToLength(c.originalRange.getEndPosition()),
lengthOfRange(c.modifiedRange).toLength(),
)));
const combined = combineTextEditInfos(diffTextEdits, textEdits);
let lastOriginalEndOffset = lengthZero;
let lastModifiedEndOffset = lengthZero;
const rangeMappings = combined.map(c => {
const modifiedStartOffset = lengthAdd(lastModifiedEndOffset, lengthDiffNonNegative(lastOriginalEndOffset, c.startOffset));
lastOriginalEndOffset = c.endOffset;
lastModifiedEndOffset = lengthAdd(modifiedStartOffset, c.newLength);
return new RangeMapping(
Range.fromPositions(lengthToPosition(c.startOffset), lengthToPosition(c.endOffset)),
Range.fromPositions(lengthToPosition(modifiedStartOffset), lengthToPosition(lastModifiedEndOffset)),
);
});
const changes = lineRangeMappingFromRangeMappings(
rangeMappings,
originalTextModel.getLinesContent(),
modifiedTextModel.getLinesContent(),
);
return {
identical: false,
quitEarly: false,
changes,
};
}

View file

@ -0,0 +1,237 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
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 { 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 { 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';
export class ViewZoneAlignment extends Disposable {
constructor(
private readonly _originalEditor: CodeEditorWidget,
private readonly _modifiedEditor: CodeEditorWidget,
private readonly _diffModel: IObservable<DiffModel | null>,
) {
super();
let isChangingViewZones = false;
const origViewZonesChanged = observableSignalFromEvent(
'origViewZonesChanged',
e => this._originalEditor.onDidChangeViewZones((args) => { if (!isChangingViewZones) { e(args); } })
);
const modViewZonesChanged = observableSignalFromEvent(
'modViewZonesChanged',
e => this._modifiedEditor.onDidChangeViewZones((args) => { if (!isChangingViewZones) { e(args); } })
);
const alignmentViewZoneIdsOrig = new Set<string>();
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; }
origViewZonesChanged.read(reader);
modViewZonesChanged.read(reader);
return computeRangeAlignment(this._originalEditor, this._modifiedEditor, diff.changes, alignmentViewZoneIdsOrig, alignmentViewZoneIdsMod);
});
function createFakeLinesDiv(): HTMLElement {
const r = document.createElement('div');
r.className = 'diagonal-fill';
return r;
}
const alignmentViewZones = derived<{ orig: IViewZone[]; mod: IViewZone[] }>('alignment viewzones', (reader) => {
const alignments_ = alignments.read(reader);
const origViewZones: IViewZone[] = [];
const modViewZones: IViewZone[] = [];
if (alignments_) {
for (const a of alignments_) {
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 };
});
this._register(autorunWithStore2('alignment viewzones', (reader) => {
const alignmentViewZones_ = alignmentViewZones.read(reader);
isChangingViewZones = true;
this._originalEditor.changeViewZones((aOrig) => {
for (const id of alignmentViewZoneIdsOrig) { aOrig.removeZone(id); }
alignmentViewZoneIdsOrig.clear();
for (const z of alignmentViewZones_.orig) { alignmentViewZoneIdsOrig.add(aOrig.addZone(z)); }
});
this._modifiedEditor.changeViewZones(aMod => {
for (const id of alignmentViewZoneIdsMod) { aMod.removeZone(id); }
alignmentViewZoneIdsMod.clear();
for (const z of alignmentViewZones_.mod) { alignmentViewZoneIdsMod.add(aMod.addZone(z)); }
});
isChangingViewZones = false;
}));
}
}
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;
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 });
}
}
}
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 {
originalRange: LineRange;
modifiedRange: LineRange;
// accounts for foreign viewzones and line wrapping
originalHeightInPx: number;
modifiedHeightInPx: number;
}
function computeRangeAlignment(
originalEditor: CodeEditorWidget,
modifiedEditor: CodeEditorWidget,
diffs: LineRangeMapping[],
originalEditorAlignmentViewZones: ReadonlySet<string>,
modifiedEditorAlignmentViewZones: ReadonlySet<string>,
): IRangeAlignment[] {
const originalLineHeightOverrides = new ArrayQueue(getAdditionalLineHeights(originalEditor, originalEditorAlignmentViewZones));
const modifiedLineHeightOverrides = new ArrayQueue(getAdditionalLineHeights(modifiedEditor, modifiedEditorAlignmentViewZones));
const origLineHeight = originalEditor.getOption(EditorOption.lineHeight);
const modLineHeight = modifiedEditor.getOption(EditorOption.lineHeight);
const result: IRangeAlignment[] = [];
let lastOriginalLineNumber = 0;
let lastModifiedLineNumber = 0;
function handleAlignmentsOutsideOfDiffs(untilOriginalLineNumberExclusive: number, untilModifiedLineNumberExclusive: number) {
while (true) {
let origNext = originalLineHeightOverrides.peek();
let modNext = modifiedLineHeightOverrides.peek();
if (origNext && origNext.lineNumber >= untilOriginalLineNumberExclusive) {
origNext = undefined;
}
if (modNext && modNext.lineNumber >= untilModifiedLineNumberExclusive) {
modNext = undefined;
}
if (!origNext && !modNext) {
break;
}
const distOrig = origNext ? origNext.lineNumber - lastOriginalLineNumber : Number.MAX_VALUE;
const distNext = modNext ? modNext.lineNumber - lastModifiedLineNumber : Number.MAX_VALUE;
if (distOrig < distNext) {
originalLineHeightOverrides.dequeue();
modNext = {
lineNumber: origNext!.lineNumber - lastOriginalLineNumber + lastModifiedLineNumber,
heightInPx: 0,
};
} else if (distOrig > distNext) {
modifiedLineHeightOverrides.dequeue();
origNext = {
lineNumber: modNext!.lineNumber - lastModifiedLineNumber + lastOriginalLineNumber,
heightInPx: 0,
};
} else {
originalLineHeightOverrides.dequeue();
modifiedLineHeightOverrides.dequeue();
}
result.push({
originalRange: LineRange.ofLength(origNext!.lineNumber, 1),
modifiedRange: LineRange.ofLength(modNext!.lineNumber, 1),
originalHeightInPx: origLineHeight + origNext!.heightInPx,
modifiedHeightInPx: modLineHeight + modNext!.heightInPx,
});
}
}
for (const c of diffs) {
handleAlignmentsOutsideOfDiffs(c.originalRange.startLineNumber, c.modifiedRange.startLineNumber);
const originalAdditionalHeight = originalLineHeightOverrides
.takeWhile(v => v.lineNumber < c.originalRange.endLineNumberExclusive)
?.reduce((p, c) => p + c.heightInPx, 0) ?? 0;
const modifiedAdditionalHeight = modifiedLineHeightOverrides
.takeWhile(v => v.lineNumber < c.modifiedRange.endLineNumberExclusive)
?.reduce((p, c) => p + c.heightInPx, 0) ?? 0;
result.push({
originalRange: c.originalRange,
modifiedRange: c.modifiedRange,
originalHeightInPx: c.originalRange.length * origLineHeight + originalAdditionalHeight,
modifiedHeightInPx: c.modifiedRange.length * modLineHeight + modifiedAdditionalHeight,
});
lastOriginalLineNumber = c.originalRange.endLineNumberExclusive;
lastModifiedLineNumber = c.modifiedRange.endLineNumberExclusive;
}
handleAlignmentsOutsideOfDiffs(Number.MAX_VALUE, Number.MAX_VALUE);
return result;
}

View file

@ -0,0 +1,149 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EventType, addDisposableListener, addStandardDisposableListener, h } from 'vs/base/browser/dom';
import { createFastDomNode } from 'vs/base/browser/fastDomNode';
import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
import { Color } from 'vs/base/common/color';
import { Disposable } from 'vs/base/common/lifecycle';
import { IObservable, autorun, derived, observableFromEvent } from 'vs/base/common/observable';
import { autorunWithStore2 } from 'vs/base/common/observableImpl/autorun';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { DiffModel } from 'vs/editor/browser/widget/diffEditorWidget2/diffModel';
import { appendRemoveOnDispose } from 'vs/editor/browser/widget/diffEditorWidget2/utils';
import { EditorLayoutInfo } from 'vs/editor/common/config/editorOptions';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { OverviewRulerZone } from 'vs/editor/common/viewModel/overviewZoneManager';
import { defaultInsertColor, defaultRemoveColor, diffInserted, diffOverviewRulerInserted, diffOverviewRulerRemoved, diffRemoved } from 'vs/platform/theme/common/colorRegistry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
export class OverviewRulerPart extends Disposable {
public static readonly ONE_OVERVIEW_WIDTH = 15;
public static readonly ENTIRE_DIFF_OVERVIEW_WIDTH = OverviewRulerPart.ONE_OVERVIEW_WIDTH * 2;
constructor(
private readonly _originalEditor: CodeEditorWidget,
private readonly _modifiedEditor: CodeEditorWidget,
private readonly _rootElement: HTMLElement,
private readonly _diffModel: IObservable<DiffModel | null>,
private readonly _rootWidth: IObservable<number>,
private readonly _rootHeight: IObservable<number>,
private readonly _modifiedEditorLayoutInfo: IObservable<EditorLayoutInfo | null>,
public readonly renderOverviewRuler: IObservable<boolean>,
@IThemeService private readonly _themeService: IThemeService,
) {
super();
const currentColorTheme = observableFromEvent(this._themeService.onDidColorThemeChange, () => this._themeService.getColorTheme());
const currentColors = derived('colors', reader => {
const theme = currentColorTheme.read(reader);
const insertColor = theme.getColor(diffOverviewRulerInserted) || (theme.getColor(diffInserted) || defaultInsertColor).transparent(2);
const removeColor = theme.getColor(diffOverviewRulerRemoved) || (theme.getColor(diffRemoved) || defaultRemoveColor).transparent(2);
return { insertColor, removeColor };
});
const scrollTopObservable = observableFromEvent(this._modifiedEditor.onDidScrollChange, () => this._modifiedEditor.getScrollTop());
const scrollHeightObservable = observableFromEvent(this._modifiedEditor.onDidScrollChange, () => this._modifiedEditor.getScrollHeight());
// overview ruler
this._register(autorunWithStore2('create diff editor overview ruler if enabled', (reader, store) => {
if (!this.renderOverviewRuler.read(reader)) {
return;
}
const viewportDomElement = createFastDomNode(document.createElement('div'));
viewportDomElement.setClassName('diffViewport');
viewportDomElement.setPosition('absolute');
const diffOverviewRoot = h('div.diffOverview', {
style: { position: 'absolute', top: '0px', width: OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH + 'px' }
}).root;
store.add(appendRemoveOnDispose(diffOverviewRoot, viewportDomElement.domNode));
store.add(addStandardDisposableListener(diffOverviewRoot, EventType.POINTER_DOWN, (e) => {
this._modifiedEditor.delegateVerticalScrollbarPointerDown(e);
}));
store.add(addDisposableListener(diffOverviewRoot, EventType.MOUSE_WHEEL, (e: IMouseWheelEvent) => {
this._modifiedEditor.delegateScrollFromMouseWheelEvent(e);
}, { passive: false }));
store.add(appendRemoveOnDispose(this._rootElement, diffOverviewRoot));
store.add(autorunWithStore2('recreate overview rules when model changes', (reader, store) => {
const m = this._diffModel.read(reader);
const originalOverviewRuler = this._originalEditor.createOverviewRuler('original diffOverviewRuler');
if (originalOverviewRuler) {
store.add(originalOverviewRuler);
store.add(appendRemoveOnDispose(diffOverviewRoot, originalOverviewRuler.getDomNode()));
}
const modifiedOverviewRuler = this._modifiedEditor.createOverviewRuler('modified diffOverviewRuler');
if (modifiedOverviewRuler) {
store.add(modifiedOverviewRuler);
store.add(appendRemoveOnDispose(diffOverviewRoot, modifiedOverviewRuler.getDomNode()));
}
if (!originalOverviewRuler || !modifiedOverviewRuler) {
// probably no model
return;
}
store.add(autorun('set overview ruler zones', (reader) => {
const colors = currentColors.read(reader);
const diff = m?.diff.read(reader)?.changes;
function createZones(ranges: LineRange[], color: Color) {
return ranges
.filter(d => d.length > 0)
.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));
}));
store.add(autorun('layout overview ruler', (reader) => {
const height = this._rootHeight.read(reader);
const width = this._rootWidth.read(reader);
const layoutInfo = this._modifiedEditorLayoutInfo.read(reader);
if (layoutInfo) {
const freeSpace = OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH - 2 * OverviewRulerPart.ONE_OVERVIEW_WIDTH;
originalOverviewRuler.setLayout({
top: 0,
height: height,
right: freeSpace + OverviewRulerPart.ONE_OVERVIEW_WIDTH,
width: OverviewRulerPart.ONE_OVERVIEW_WIDTH,
});
modifiedOverviewRuler.setLayout({
top: 0,
height: height,
right: 0,
width: OverviewRulerPart.ONE_OVERVIEW_WIDTH,
});
const scrollTop = scrollTopObservable.read(reader);
const scrollHeight = scrollHeightObservable.read(reader);
const computedAvailableSize = Math.max(0, layoutInfo.height);
const computedRepresentableSize = Math.max(0, computedAvailableSize - 2 * 0);
const computedRatio = scrollHeight > 0 ? (computedRepresentableSize / scrollHeight) : 0;
const computedSliderSize = Math.max(0, Math.floor(layoutInfo.height * computedRatio));
const computedSliderPosition = Math.floor(scrollTop * computedRatio);
viewportDomElement.setTop(computedSliderPosition);
viewportDomElement.setHeight(computedSliderSize);
} else {
viewportDomElement.setTop(0);
viewportDomElement.setHeight(0);
}
diffOverviewRoot.style.height = height + 'px';
diffOverviewRoot.style.left = (width - OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH) + 'px';
viewportDomElement.setWidth(OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH);
}));
}));
}));
}
}

View file

@ -0,0 +1,61 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.diff-hidden-lines.showTop {
margin: -5px 0;
}
.diff-hidden-lines.showTop .top {
height: 7px;
background-color: var(--vscode-diffEditor-unchangedRegionBackground);
mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='7' viewBox='0 0 10 10' preserveAspectRatio='none'> <polygon points='0,10 5,0 10,10' style='fill:white' /> </svg>") repeat 0px 0px;
-webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='7' viewBox='0 0 10 10' preserveAspectRatio='none'> <polygon points='0,10 5,0 10,10' style='fill:white' /> </svg>") repeat 0px 0px;
}
.diff-hidden-lines.showBottom .bottom {
height: 7px;
background-color: var(--vscode-diffEditor-unchangedRegionBackground);
mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='7' viewBox='0 0 10 10' preserveAspectRatio='none'> <polygon points='0,0 5,10 10,0' style='fill:white' /> </svg>") repeat 10px 0px;
-webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='7' viewBox='0 0 10 10' preserveAspectRatio='none'> <polygon points='0,0 5,10 10,0' style='fill:white' /> </svg>") repeat 10px 0px;
}
.diff-hidden-lines .center {
background: var(--vscode-diffEditor-unchangedRegionBackground);
padding: 0px 3px;
overflow: hidden;
display: block;
text-overflow: ellipsis;
white-space: nowrap;
}
.diff-hidden-lines .center > span,
.diff-hidden-lines .center > a {
user-select: none;
-webkit-user-select: none;
white-space: nowrap;
}
.diff-hidden-lines .center > a {
text-decoration: none;
}
.diff-hidden-lines .center > a:hover {
cursor: pointer;
color: var(--vscode-editorLink-activeForeground) !important;
}
.diff-hidden-lines .center > a:hover .codicon {
color: var(--vscode-editorLink-activeForeground) !important;
}
.diff-hidden-lines .center .codicon {
vertical-align: middle;
color: currentColor !important;
color: var(--vscode-editorCodeLens-foreground);
}
.merge-editor-conflict-actions > a:hover .codicon::before {
cursor: pointer;
}

View file

@ -0,0 +1,224 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { $, h, reset } from 'vs/base/browser/dom';
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
import { Disposable } from 'vs/base/common/lifecycle';
import { IObservable, transaction, constObservable } from 'vs/base/common/observable';
import { autorun, autorunWithStore2 } from 'vs/base/common/observableImpl/autorun';
import { isDefined } from 'vs/base/common/types';
import { ICodeEditor, IOverlayWidget, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { DiffModel } from 'vs/editor/browser/widget/diffEditorWidget2/diffModel';
export class UnchangedRangesFeature extends Disposable {
constructor(
private readonly _originalEditor: CodeEditorWidget,
private readonly _modifiedEditor: CodeEditorWidget,
private readonly _diffModel: IObservable<DiffModel | null>,
) {
super();
const unchangedRegionViewZoneIdsOrig: string[] = [];
const unchangedRegionViewZoneIdsMod: string[] = [];
this._register(this._originalEditor.onDidChangeCursorPosition(e => {
const m = this._diffModel.get();
transaction(tx => {
for (const s of this._originalEditor.getSelections() || []) {
m?.revealOriginalLine(s.getStartPosition().lineNumber, tx);
m?.revealOriginalLine(s.getEndPosition().lineNumber, tx);
}
});
}));
this._register(this._modifiedEditor.onDidChangeCursorPosition(e => {
const m = this._diffModel.get();
transaction(tx => {
for (const s of this._modifiedEditor.getSelections() || []) {
m?.revealModifiedLine(s.getStartPosition().lineNumber, tx);
m?.revealModifiedLine(s.getEndPosition().lineNumber, tx);
}
});
}));
this._register(autorunWithStore2('update folded unchanged regions', (reader, store) => {
const unchangedRegions = this._diffModel.read(reader)?.unchangedRegions.read(reader);
if (!unchangedRegions) {
return;
}
// TODO@hediet This might cause unnecessary updates of alignment viewzones if this runs too late
this._originalEditor.changeViewZones((aOrig) => {
this._modifiedEditor.changeViewZones(aMod => {
for (const id of unchangedRegionViewZoneIdsOrig) {
aOrig.removeZone(id);
}
unchangedRegionViewZoneIdsOrig.length = 0;
for (const id of unchangedRegionViewZoneIdsMod) {
aMod.removeZone(id);
}
unchangedRegionViewZoneIdsMod.length = 0;
for (const r of unchangedRegions) {
const atTop = r.modifiedLineNumber !== 1;
const atBottom = r.modifiedRange.endLineNumberExclusive !== this._modifiedEditor.getModel()!.getLineCount() + 1;
const hiddenOriginalRange = r.getHiddenOriginalRange(reader);
const hiddenModifiedRange = r.getHiddenModifiedRange(reader);
if (hiddenOriginalRange.isEmpty) {
continue;
}
store.add(new CollapsedCodeActionsContentWidget(this._originalEditor, aOrig, hiddenOriginalRange.startLineNumber - 1, 30, constObservable<IContentWidgetAction[]>([
{
text: `${hiddenOriginalRange.length} Lines Hidden`
},
{
text: '$(chevron-up) Show More',
async action() { r.showMoreAbove(undefined); },
},
{
text: '$(chevron-down) Show More',
async action() { r.showMoreBelow(undefined); },
},
{
text: '$(close) Show All',
async action() { r.showAll(undefined); },
}
]), unchangedRegionViewZoneIdsOrig, atTop, atBottom));
store.add(new CollapsedCodeActionsContentWidget(this._modifiedEditor, aMod, hiddenModifiedRange.startLineNumber - 1, 30, constObservable<IContentWidgetAction[]>([
{
text: '$(chevron-up) Show More',
async action() { r.showMoreAbove(undefined); },
},
{
text: '$(chevron-down) Show More',
async action() { r.showMoreBelow(undefined); },
},
{
text: '$(close) Show All',
async action() { r.showAll(undefined); },
}
]), unchangedRegionViewZoneIdsMod, atTop, atBottom));
}
});
});
this._originalEditor.setHiddenAreas(unchangedRegions.map(r => r.getHiddenOriginalRange(reader).toInclusiveRange()).filter(isDefined));
this._modifiedEditor.setHiddenAreas(unchangedRegions.map(r => r.getHiddenModifiedRange(reader).toInclusiveRange()).filter(isDefined));
}));
}
}
// TODO@hediet avoid code duplication with FixedZoneWidget in merge editor
abstract class FixedZoneWidget extends Disposable {
private static counter = 0;
private readonly overlayWidgetId = `fixedZoneWidget-${FixedZoneWidget.counter++}`;
private readonly viewZoneId: string;
protected readonly widgetDomNode = h('div.fixed-zone-widget').root;
private readonly overlayWidget: IOverlayWidget = {
getId: () => this.overlayWidgetId,
getDomNode: () => this.widgetDomNode,
getPosition: () => null
};
constructor(
private readonly editor: ICodeEditor,
viewZoneAccessor: IViewZoneChangeAccessor,
afterLineNumber: number,
height: number,
viewZoneIdsToCleanUp: string[],
) {
super();
this.viewZoneId = viewZoneAccessor.addZone({
domNode: document.createElement('div'),
afterLineNumber: afterLineNumber,
heightInPx: height,
onComputedHeight: (height) => {
this.widgetDomNode.style.height = `${height}px`;
},
onDomNodeTop: (top) => {
this.widgetDomNode.style.top = `${top}px`;
},
showInHiddenAreas: true,
});
viewZoneIdsToCleanUp.push(this.viewZoneId);
this.widgetDomNode.style.left = this.editor.getLayoutInfo().contentLeft + 'px';
this.editor.addOverlayWidget(this.overlayWidget);
this._register({
dispose: () => {
this.editor.removeOverlayWidget(this.overlayWidget);
},
});
}
}
class CollapsedCodeActionsContentWidget extends FixedZoneWidget {
private readonly _domNode = h('div.diff-hidden-lines', { className: [this.showTopZigZag ? 'showTop' : '', this.showBottomZigZag ? 'showBottom' : ''].join(' ') }, [
h('div.top'),
h('div.center@content'),
h('div.bottom'),
]);
constructor(
editor: ICodeEditor,
viewZoneAccessor: IViewZoneChangeAccessor,
afterLineNumber: number,
height: number,
items: IObservable<IContentWidgetAction[]>,
viewZoneIdsToCleanUp: string[],
public readonly showTopZigZag: boolean,
public readonly showBottomZigZag: boolean,
) {
super(editor, viewZoneAccessor, afterLineNumber, height, viewZoneIdsToCleanUp);
this.widgetDomNode.appendChild(this._domNode.root);
this._register(autorun('update commands', (reader) => {
const i = items.read(reader);
this.setState(i);
}));
}
private setState(items: IContentWidgetAction[]) {
const children: HTMLElement[] = [];
let isFirst = true;
for (const item of items) {
if (isFirst) {
isFirst = false;
} else {
children.push($('span', undefined, '\u00a0|\u00a0'));
}
const title = renderLabelWithIcons(item.text);
if (item.action) {
children.push($('a', { title: item.tooltip, role: 'button', onclick: () => item.action!() }, ...title));
} else {
children.push($('span', { title: item.tooltip }, ...title));
}
}
reset(this._domNode.content, ...children);
}
}
interface IContentWidgetAction {
text: string;
tooltip?: string;
action?: () => Promise<void>;
}

View file

@ -0,0 +1,121 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
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 { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IModelDeltaDecoration } from 'vs/editor/common/model';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
export function joinCombine<T>(arr1: readonly T[], arr2: readonly T[], keySelector: (val: T) => number, combine: (v1: T, v2: T) => T): readonly T[] {
if (arr1.length === 0) {
return arr2;
}
if (arr2.length === 0) {
return arr1;
}
const result: T[] = [];
let i = 0;
let j = 0;
while (i < arr1.length && j < arr2.length) {
const val1 = arr1[i];
const val2 = arr2[j];
const key1 = keySelector(val1);
const key2 = keySelector(val2);
if (key1 < key2) {
result.push(val1);
i++;
} else if (key1 > key2) {
result.push(val2);
j++;
} else {
result.push(combine(val1, val2));
i++;
j++;
}
}
while (i < arr1.length) {
result.push(arr1[i]);
i++;
}
while (j < arr2.length) {
result.push(arr2[j]);
j++;
}
return result;
}
// TODO make utility
export function applyObservableDecorations(editor: ICodeEditor, decorations: IObservable<IModelDeltaDecoration[]>): IDisposable {
const d = new DisposableStore();
const decorationsCollection = editor.createDecorationsCollection();
d.add(autorun(`Apply decorations from ${decorations.debugName}`, reader => {
const d = decorations.read(reader);
decorationsCollection.set(d);
}));
d.add({
dispose: () => {
decorationsCollection.clear();
}
});
return d;
}
export function appendRemoveOnDispose(parent: HTMLElement, child: HTMLElement) {
parent.appendChild(child);
return toDisposable(() => {
parent.removeChild(child);
});
}
export function observableConfigValue<T>(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable<T> {
return observableFromEvent(
(handleChange) => configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(key)) {
handleChange(e);
}
}),
() => configurationService.getValue<T>(key) ?? defaultValue,
);
}
export class ObservableElementSizeObserver extends Disposable {
private readonly elementSizeObserver: ElementSizeObserver;
private readonly _width: ISettableObservable<number>;
public get width(): ISettableObservable<number> { return this._width; }
private readonly _height: ISettableObservable<number>;
public get height(): ISettableObservable<number> { return this._height; }
constructor(element: HTMLElement | null, dimension: IDimension | undefined) {
super();
this.elementSizeObserver = this._register(new ElementSizeObserver(element, dimension));
this._width = observableValue('width', this.elementSizeObserver.getWidth());
this._height = observableValue('height', this.elementSizeObserver.getHeight());
this._register(this.elementSizeObserver.onDidChange(e => transaction(tx => {
this._width.set(this.elementSizeObserver.getWidth(), tx);
this._height.set(this.elementSizeObserver.getHeight(), tx);
})));
}
public observe(dimension?: IDimension): void {
this.elementSizeObserver.observe(dimension);
}
public setAutomaticLayout(automaticLayout: boolean): void {
if (automaticLayout) {
this.elementSizeObserver.startObserving();
} else {
this.elementSizeObserver.stopObserving();
}
}
}

View file

@ -55,7 +55,7 @@ export class DiffNavigator extends Disposable implements IDiffNavigator {
readonly onDidUpdate: Event<this> = this._onDidUpdate.event;
private disposed: boolean;
private revealFirst: boolean;
public revealFirst: boolean;
private nextIdx: number;
private ranges: IDiffRange[];
private ignoreSelectionChange: boolean;
@ -78,8 +78,6 @@ export class DiffNavigator extends Disposable implements IDiffNavigator {
this.ignoreSelectionChange = false;
this.revealFirst = Boolean(this._options.alwaysRevealFirst);
// hook up to diff editor for diff, disposal, and caret move
this._register(this._editor.onDidDispose(() => this.dispose()));
this._register(this._editor.onDidUpdateDiff(() => this._onDiffUpdated()));
if (this._options.followsCaret) {
@ -91,11 +89,6 @@ export class DiffNavigator extends Disposable implements IDiffNavigator {
this.nextIdx = -1;
}));
}
if (this._options.alwaysRevealFirst) {
this._register(this._editor.getModifiedEditor().onDidChangeModel((e) => {
this.revealFirst = true;
}));
}
// init things
this._init();

View file

@ -6,7 +6,9 @@
import { Emitter, Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { StopWatch } from 'vs/base/common/stopwatch';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { IDocumentDiff, IDocumentDiffProvider, IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider';
import { LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
import { ITextModel } from 'vs/editor/common/model';
import { DiffAlgorithmName, IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@ -35,6 +37,26 @@ export class WorkerBasedDocumentDiffProvider implements IDocumentDiffProvider, I
return this.diffAlgorithm.computeDiff(original, modified, options);
}
// This significantly speeds up the case when the original file is empty
if (original.getLineCount() === 1 && original.getLineMaxColumn(1) === 1) {
return {
changes: [
new LineRangeMapping(
new LineRange(1, 1),
new LineRange(1, modified.getLineCount()),
[
new RangeMapping(
original.getFullModelRange(),
modified.getFullModelRange(),
)
]
)
],
identical: false,
quitEarly: false,
};
}
const sw = StopWatch.create(true);
const result = await this.editorWorkerService.computeDiff(original.uri, modified.uri, options, this.diffAlgorithm);
const timeMs = sw.elapsed();

View file

@ -202,6 +202,16 @@ const editorConfiguration: IConfigurationNode = {
],
tags: ['experimental'],
},
'diffEditor.experimental.collapseUnchangedRegions': {
type: 'boolean',
default: false,
description: nls.localize('collapseUnchangedRegions', "Controls whether the diff editor shows unchanged regions. Only works when 'diffEditor.experimental.useVersion2' is set."),
},
'diffEditor.experimental.useVersion2': {
type: 'boolean',
default: false,
description: nls.localize('useVersion2', "Controls whether the diff editor uses the new or the old implementation."),
}
}
};

View file

@ -794,6 +794,13 @@ export interface IDiffEditorBaseOptions {
* Whether the diff editor aria label should be verbose.
*/
accessibilityVerbose?: boolean;
experimental?: {
/**
* Defaults to false.
*/
collapseUnchangedRegions?: boolean;
};
}
/**

View file

@ -4,11 +4,16 @@
*--------------------------------------------------------------------------------------------*/
import { BugIndicatingError } from 'vs/base/common/errors';
import { Range } from 'vs/editor/common/core/range';
/**
* A range of lines (1-based).
*/
export class LineRange {
public static fromRange(range: Range): LineRange {
return new LineRange(range.startLineNumber, range.endLineNumber);
}
/**
* @param lineRanges An array of sorted line ranges.
*/
@ -78,6 +83,10 @@ export class LineRange {
return result;
}
public static ofLength(startLineNumber: number, length: number): LineRange {
return new LineRange(startLineNumber, startLineNumber + length);
}
/**
* The start line number.
*/
@ -154,6 +163,10 @@ export class LineRange {
return undefined;
}
public intersectsStrict(other: LineRange): boolean {
return this.startLineNumber < other.endLineNumberExclusive && other.startLineNumber < this.endLineNumberExclusive;
}
public overlapOrTouch(other: LineRange): boolean {
return this.startLineNumber <= other.endLineNumberExclusive && other.startLineNumber <= this.endLineNumberExclusive;
}
@ -161,4 +174,15 @@ export class LineRange {
public equals(b: LineRange): boolean {
return this.startLineNumber === b.startLineNumber && this.endLineNumberExclusive === b.endLineNumberExclusive;
}
public toInclusiveRange(): Range | null {
if (this.isEmpty) {
return null;
}
return new Range(this.startLineNumber, 1, this.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER);
}
public toExclusiveRange(): Range {
return new Range(this.startLineNumber, 1, this.endLineNumberExclusive, 1);
}
}

View file

@ -32,6 +32,34 @@ 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[] {
const result: LineRangeMapping[] = [];
let lastOriginalEndLineNumber = 1;
let lastModifiedEndLineNumber = 1;
for (const m of mapping) {
const r = new LineRangeMapping(
new LineRange(lastOriginalEndLineNumber, m.originalRange.startLineNumber),
new LineRange(lastModifiedEndLineNumber, m.modifiedRange.startLineNumber),
undefined
);
if (!r.modifiedRange.isEmpty) {
result.push(r);
}
lastOriginalEndLineNumber = m.originalRange.endLineNumberExclusive;
lastModifiedEndLineNumber = m.modifiedRange.endLineNumberExclusive;
}
const r = new LineRangeMapping(
new LineRange(lastOriginalEndLineNumber, originalLineCount + 1),
new LineRange(lastModifiedEndLineNumber, modifiedLineCount + 1),
undefined
);
if (!r.modifiedRange.isEmpty) {
result.push(r);
}
return result;
}
/**
* The line range in the original text model.
*/

View file

@ -3,9 +3,24 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Length, lengthAdd, lengthDiffNonNegative, lengthLessThanEqual, LengthObj, lengthToObj, toLength } from './length';
import { Range } from 'vs/editor/common/core/range';
import { Length, lengthAdd, lengthDiffNonNegative, lengthLessThanEqual, LengthObj, lengthOfString, lengthToObj, positionToLength, toLength } from './length';
import { IModelContentChange } from 'vs/editor/common/textModelEvents';
export class TextEditInfo {
public static fromModelContentChanges(changes: IModelContentChange[]): TextEditInfo[] {
// Must be sorted in ascending order
const edits = changes.map(c => {
const range = Range.lift(c.range);
return new TextEditInfo(
positionToLength(range.getStartPosition()),
positionToLength(range.getEndPosition()),
lengthOfString(c.text)
);
}).reverse();
return edits;
}
constructor(
public readonly startOffset: Length,
public readonly endOffset: Length,

View file

@ -14,7 +14,7 @@ import { ResolvedLanguageConfiguration } from 'vs/editor/common/languages/langua
import { AstNode, AstNodeKind } from './ast';
import { TextEditInfo } from './beforeEditPositionMapper';
import { LanguageAgnosticBracketTokens } from './brackets';
import { Length, lengthAdd, lengthGreaterThanEqual, lengthLessThan, lengthLessThanEqual, lengthOfString, lengthsToRange, lengthZero, positionToLength, toLength } from './length';
import { Length, lengthAdd, lengthGreaterThanEqual, lengthLessThan, lengthLessThanEqual, lengthsToRange, lengthZero, positionToLength, toLength } from './length';
import { parseDocument } from './parser';
import { DenseKeyProvider } from './smallImmutableSet';
import { FastTokenizer, TextBufferTokenizer } from './tokenizer';
@ -103,16 +103,7 @@ export class BracketPairsTree extends Disposable {
}
public handleContentChanged(change: IModelContentChangedEvent) {
// Must be sorted in ascending order
const edits = change.changes.map(c => {
const range = Range.lift(c.range);
return new TextEditInfo(
positionToLength(range.getStartPosition()),
positionToLength(range.getEndPosition()),
lengthOfString(c.text)
);
}).reverse();
const edits = TextEditInfo.fromModelContentChanges(change.changes);
this.handleEdits(edits, false);
}

View file

@ -216,6 +216,14 @@ export function lengthsToRange(lengthStart: Length, lengthEnd: Length): Range {
return new Range(lineCount + 1, colCount + 1, lineCount2 + 1, colCount2 + 1);
}
export function lengthOfRange(range: Range): LengthObj {
if (range.startLineNumber === range.endLineNumber) {
return new LengthObj(0, range.endColumn - range.startColumn);
} else {
return new LengthObj(range.endLineNumber - range.startLineNumber, range.endColumn - 1);
}
}
export function lengthCompare(length1: Length, length2: Length): number {
const l1 = length1 as any as number;
const l2 = length2 as any as number;

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

@ -2374,10 +2374,12 @@ declare namespace monaco.editor {
*/
readonly changes: LineRangeMapping[];
}
/**
* A range of lines (1-based).
*/
export class LineRange {
static fromRange(range: Range): LineRange;
/**
* @param lineRanges An array of sorted line ranges.
*/
@ -2387,6 +2389,7 @@ declare namespace monaco.editor {
* @param lineRanges2 Must be sorted.
*/
static join(lineRanges1: readonly LineRange[], lineRanges2: readonly LineRange[]): readonly LineRange[];
static ofLength(startLineNumber: number, length: number): LineRange;
/**
* The start line number.
*/
@ -2422,14 +2425,18 @@ declare namespace monaco.editor {
* If the ranges don't even touch, the result is undefined.
*/
intersect(other: LineRange): LineRange | undefined;
intersectsStrict(other: LineRange): boolean;
overlapOrTouch(other: LineRange): boolean;
equals(b: LineRange): boolean;
toInclusiveRange(): Range | null;
toExclusiveRange(): Range;
}
/**
* 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[];
/**
* The line range in the original text model.
*/
@ -3860,6 +3867,12 @@ declare namespace monaco.editor {
* Whether the diff editor aria label should be verbose.
*/
accessibilityVerbose?: boolean;
experimental?: {
/**
* Defaults to false.
*/
collapseUnchangedRegions?: boolean;
};
}
/**

View file

@ -424,6 +424,8 @@ export const diffRemovedOutline = registerColor('diffEditor.removedTextBorder',
export const diffBorder = registerColor('diffEditor.border', { dark: null, light: null, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('diffEditorBorder', 'Border color between the two text editors.'));
export const diffDiagonalFill = registerColor('diffEditor.diagonalFill', { dark: '#cccccc33', light: '#22222233', hcDark: null, hcLight: null }, nls.localize('diffDiagonalFill', "Color of the diff editor's diagonal fill. The diagonal fill is used in side-by-side diff views."));
export const diffUnchangedRegionBackground = registerColor('diffEditor.unchangedRegionBackground', { dark: '#000000', light: '#e4e4e4', hcDark: null, hcLight: null }, nls.localize('diffEditor.unchangedRegionBackground', "The color of unchanged blocks in diff editor."));
/**
* List and tree colors
*/

View file

@ -68,6 +68,7 @@ import { UntitledTextEditorInputSerializer, UntitledTextEditorWorkingCopyEditorH
import { DynamicEditorConfigurations } from 'vs/workbench/browser/parts/editor/editorConfiguration';
import { AccessibilityStatus } from 'vs/workbench/browser/parts/editor/accessibilityStatus';
import { ToggleTabsVisibilityAction } from 'vs/workbench/browser/actions/layoutActions';
import 'vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.contribution';
//#region Editor Registrations

View file

@ -385,10 +385,7 @@ function registerDiffEditorCommands(): void {
const activeTextDiffEditor = getActiveTextDiffEditor(accessor);
if (activeTextDiffEditor) {
const navigator = activeTextDiffEditor.getDiffNavigator();
if (navigator) {
next ? navigator.next() : navigator.previous();
}
activeTextDiffEditor.getControl()?.goToDiff(next ? 'next' : 'previous');
}
}

View file

@ -7,13 +7,12 @@ import { localize } from 'vs/nls';
import { deepClone } from 'vs/base/common/objects';
import { isObject, assertIsDefined, withUndefinedAsNull, withNullAsUndefined } from 'vs/base/common/types';
import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser';
import { IDiffEditorOptions, EditorOption, IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IDiffEditorOptions, IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions';
import { AbstractTextEditor, IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor';
import { TEXT_DIFF_EDITOR_ID, IEditorFactoryRegistry, EditorExtensions, ITextDiffEditorPane, IEditorOpenContext, EditorInputCapabilities, isEditorInput, isTextEditorViewState, createTooLargeFileError } from 'vs/workbench/common/editor';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { DiffNavigator } from 'vs/editor/browser/widget/diffNavigator';
import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget';
import { TextDiffEditorModel } from 'vs/workbench/common/editor/textDiffEditorModel';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@ -37,17 +36,17 @@ import { ByteSize, FileOperationError, FileOperationResult, IFileService, TooLar
import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash';
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
import { StopWatch } from 'vs/base/common/stopwatch';
import { DiffEditorWidget2 } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2';
/**
* The text editor that leverages the diff text editor for the editing experience.
*/
export class TextDiffEditor extends AbstractTextEditor<IDiffEditorViewState> implements ITextDiffEditorPane {
static readonly ID = TEXT_DIFF_EDITOR_ID;
private static widgetCounter = 0; // Just for debugging
private diffEditorControl: IDiffEditor | undefined = undefined;
private diffNavigator: DiffNavigator | undefined;
private readonly diffNavigatorDisposables = this._register(new DisposableStore());
private inputLifecycleStopWatch: StopWatch | undefined = undefined;
@ -86,7 +85,18 @@ export class TextDiffEditor extends AbstractTextEditor<IDiffEditorViewState> imp
}
protected override createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): void {
this.diffEditorControl = this._register(this.instantiationService.createInstance(DiffEditorWidget, parent, configuration, {}));
TextDiffEditor.widgetCounter++;
let useVersion2 = this.textResourceConfigurationService.getValue(undefined, 'diffEditor.experimental.useVersion2');
if (useVersion2 === 'first') {
// This allows to have both the old and new diff editor next to each other - just for debugging
useVersion2 = TextDiffEditor.widgetCounter === 1;
}
if (useVersion2) {
this.diffEditorControl = this._register(this.instantiationService.createInstance(DiffEditorWidget2, parent, configuration, {}));
} else {
this.diffEditorControl = this._register(this.instantiationService.createInstance(DiffEditorWidget, parent, configuration, {}));
}
}
protected updateEditorControlOptions(options: ICodeEditorOptions): void {
@ -137,12 +147,9 @@ export class TextDiffEditor extends AbstractTextEditor<IDiffEditorViewState> imp
optionsGotApplied = applyTextEditorOptions(options, control, ScrollType.Immediate);
}
// Diff navigator
this.diffNavigator = this.instantiationService.createInstance(DiffNavigator, control, {
alwaysRevealFirst: !optionsGotApplied && !hasPreviousViewState, // only reveal first change if we had no options or viewstate
findResultLoop: this.getMainControl()?.getOption(EditorOption.find).loop
});
this.diffNavigatorDisposables.add(this.diffNavigator);
if (!optionsGotApplied && !hasPreviousViewState) {
control.revealFirstDiff();
}
// Since the resolved model provides information about being readonly
// or not, we apply it here to the editor even though the editor input
@ -335,10 +342,6 @@ export class TextDiffEditor extends AbstractTextEditor<IDiffEditorViewState> imp
});
}
getDiffNavigator(): DiffNavigator | undefined {
return this.diffNavigator;
}
override getControl(): IDiffEditor | undefined {
return this.diffEditorControl;
}