mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 21:09:43 +00:00
First draft of diff editor v2.
This commit is contained in:
parent
d939e5d08f
commit
397f2e7c34
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -1216,6 +1216,16 @@ export interface IDiffEditor extends editorCommon.IEditor {
|
|||
* @internal
|
||||
*/
|
||||
setBoundarySashes(sashes: IBoundarySashes): void;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
goToDiff(target: 'next' | 'previous'): void;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
revealFirstDiff(): unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
});
|
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
});
|
|
@ -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!),
|
||||
},
|
||||
};
|
||||
}
|
344
src/vs/editor/browser/widget/diffEditorWidget2/diffModel.ts
Normal file
344
src/vs/editor/browser/widget/diffEditorWidget2/diffModel.ts
Normal 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,
|
||||
};
|
||||
}
|
237
src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts
Normal file
237
src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts
Normal 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;
|
||||
}
|
|
@ -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);
|
||||
}));
|
||||
}));
|
||||
}));
|
||||
}
|
||||
}
|
61
src/vs/editor/browser/widget/diffEditorWidget2/style.css
Normal file
61
src/vs/editor/browser/widget/diffEditorWidget2/style.css
Normal 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;
|
||||
}
|
|
@ -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>;
|
||||
}
|
121
src/vs/editor/browser/widget/diffEditorWidget2/utils.ts
Normal file
121
src/vs/editor/browser/widget/diffEditorWidget2/utils.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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."),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -794,6 +794,13 @@ export interface IDiffEditorBaseOptions {
|
|||
* Whether the diff editor aria label should be verbose.
|
||||
*/
|
||||
accessibilityVerbose?: boolean;
|
||||
|
||||
experimental?: {
|
||||
/**
|
||||
* Defaults to false.
|
||||
*/
|
||||
collapseUnchangedRegions?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
13
src/vs/monaco.d.ts
vendored
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue