mirror of
https://github.com/Microsoft/vscode
synced 2024-10-02 17:32:41 +00:00
Implements #206808
This commit is contained in:
parent
15715aedf0
commit
b7d35c8bdd
|
@ -15,6 +15,7 @@
|
|||
"contribEditorContentMenu",
|
||||
"contribMergeEditorMenus",
|
||||
"contribMultiDiffEditorMenus",
|
||||
"contribDiffEditorGutterToolBarMenus",
|
||||
"contribSourceControlHistoryItemGroupMenu",
|
||||
"contribSourceControlHistoryItemMenu",
|
||||
"contribSourceControlInputBoxMenu",
|
||||
|
@ -188,6 +189,18 @@
|
|||
"category": "Git",
|
||||
"enablement": "!operationInProgress"
|
||||
},
|
||||
{
|
||||
"command": "git.diff.stageHunk",
|
||||
"title": "%command.stageBlock%",
|
||||
"category": "Git",
|
||||
"icon": "$(plus)"
|
||||
},
|
||||
{
|
||||
"command": "git.diff.stageSelection",
|
||||
"title": "%command.stageSelection%",
|
||||
"category": "Git",
|
||||
"icon": "$(plus)"
|
||||
},
|
||||
{
|
||||
"command": "git.revertSelectedRanges",
|
||||
"title": "%command.revertSelectedRanges%",
|
||||
|
@ -2047,6 +2060,20 @@
|
|||
"when": "scmProvider == git && scmResourceGroup == index"
|
||||
}
|
||||
],
|
||||
"diffEditor/gutter/hunk": [
|
||||
{
|
||||
"command": "git.diff.stageHunk",
|
||||
"group": "primary@10",
|
||||
"when": "diffEditorOriginalUri =~ /^git\\:.*%22ref%22%3A%22~%22%7D$/"
|
||||
}
|
||||
],
|
||||
"diffEditor/gutter/selection": [
|
||||
{
|
||||
"command": "git.diff.stageSelection",
|
||||
"group": "primary@10",
|
||||
"when": "diffEditorOriginalUri =~ /^git\\:.*%22ref%22%3A%22~%22%7D$/"
|
||||
}
|
||||
],
|
||||
"scm/change/title": [
|
||||
{
|
||||
"command": "git.stageChange",
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
"command.stageSelectedRanges": "Stage Selected Ranges",
|
||||
"command.revertSelectedRanges": "Revert Selected Ranges",
|
||||
"command.stageChange": "Stage Change",
|
||||
"command.stageSelection": "Stage Selection",
|
||||
"command.stageBlock": "Stage Block",
|
||||
"command.revertChange": "Revert Change",
|
||||
"command.unstage": "Unstage Changes",
|
||||
"command.unstageAll": "Unstage All Changes",
|
||||
|
|
|
@ -12,7 +12,7 @@ import { ForcePushMode, GitErrorCodes, Ref, RefType, Status, CommitOptions, Remo
|
|||
import { Git, Stash } from './git';
|
||||
import { Model } from './model';
|
||||
import { Repository, Resource, ResourceGroupType } from './repository';
|
||||
import { applyLineChanges, getModifiedRange, intersectDiffWithRange, invertLineChange, toLineRanges } from './staging';
|
||||
import { DiffEditorSelectionHunkToolbarContext, applyLineChanges, getModifiedRange, intersectDiffWithRange, invertLineChange, toLineRanges } from './staging';
|
||||
import { fromGitUri, toGitUri, isGitUri, toMergeUris, toMultiFileDiffEditorUris } from './uri';
|
||||
import { dispose, grep, isDefined, isDescendant, pathEquals, relativePath } from './util';
|
||||
import { GitTimelineItem } from './timelineProvider';
|
||||
|
@ -1511,6 +1511,30 @@ export class CommandCenter {
|
|||
textEditor.selections = [new Selection(firstStagedLine, 0, firstStagedLine, 0)];
|
||||
}
|
||||
|
||||
@command('git.diff.stageHunk')
|
||||
async diffStageHunk(changes: DiffEditorSelectionHunkToolbarContext): Promise<void> {
|
||||
this.diffStageHunkOrSelection(changes);
|
||||
}
|
||||
|
||||
@command('git.diff.stageSelection')
|
||||
async diffStageSelection(changes: DiffEditorSelectionHunkToolbarContext): Promise<void> {
|
||||
this.diffStageHunkOrSelection(changes);
|
||||
}
|
||||
|
||||
async diffStageHunkOrSelection(changes: DiffEditorSelectionHunkToolbarContext): Promise<void> {
|
||||
const textEditor = window.activeTextEditor;
|
||||
if (!textEditor) {
|
||||
return;
|
||||
}
|
||||
const modifiedDocument = textEditor.document;
|
||||
const modifiedUri = modifiedDocument.uri;
|
||||
if (modifiedUri.scheme !== 'file') {
|
||||
return;
|
||||
}
|
||||
const result = changes.originalWithModifiedChanges;
|
||||
await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
|
||||
}
|
||||
|
||||
@command('git.stageSelectedRanges', { diff: true })
|
||||
async stageSelectedChanges(changes: LineChange[]): Promise<void> {
|
||||
const textEditor = window.activeTextEditor;
|
||||
|
|
|
@ -142,3 +142,11 @@ export function invertLineChange(diff: LineChange): LineChange {
|
|||
originalEndLineNumber: diff.modifiedEndLineNumber
|
||||
};
|
||||
}
|
||||
|
||||
export interface DiffEditorSelectionHunkToolbarContext {
|
||||
mapping: unknown;
|
||||
/**
|
||||
* The original text with the selected modified changes applied.
|
||||
*/
|
||||
originalWithModifiedChanges: string;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
|
|||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import './registrations.contribution';
|
||||
import { DiffEditorSelectionHunkToolbarContext } from 'vs/editor/browser/widget/diffEditor/features/gutterFeature';
|
||||
|
||||
export class ToggleCollapseUnchangedRegions extends Action2 {
|
||||
constructor() {
|
||||
|
@ -165,6 +166,26 @@ export class ShowAllUnchangedRegions extends EditorAction2 {
|
|||
}
|
||||
}
|
||||
|
||||
export class RevertHunkOrSelection extends EditorAction2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'diffEditor.revert',
|
||||
title: localize2('revert', 'Revert'),
|
||||
precondition: ContextKeyExpr.has('isInDiffEditor'),
|
||||
f1: false,
|
||||
category: diffEditorCategory,
|
||||
});
|
||||
}
|
||||
|
||||
runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, arg: DiffEditorSelectionHunkToolbarContext): unknown {
|
||||
const diffEditor = findFocusedDiffEditor(accessor);
|
||||
if (diffEditor instanceof DiffEditorWidget) {
|
||||
diffEditor.revertRangeMappings(arg.mapping.innerChanges ?? []);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const accessibleDiffViewerCategory: ILocalizedString = localize2('accessibleDiffViewer', "Accessible Diff Viewer");
|
||||
|
||||
export class AccessibleDiffViewerNext extends Action2 {
|
||||
|
|
|
@ -3,64 +3,56 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IObservable, IReader, autorunHandleChanges, derivedOpts, observableFromEvent } from 'vs/base/common/observable';
|
||||
import { IReader, autorunHandleChanges, derived, derivedOpts, observableFromEvent } from 'vs/base/common/observable';
|
||||
import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration';
|
||||
import { IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser';
|
||||
import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget';
|
||||
import { IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget';
|
||||
import { OverviewRulerFeature } from 'vs/editor/browser/widget/diffEditor/features/overviewRulerFeature';
|
||||
import { EditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { IContentSizeChangedEvent } from 'vs/editor/common/editorCommon';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { DiffEditorOptions } from '../diffEditorOptions';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
|
||||
export class DiffEditorEditors extends Disposable {
|
||||
public readonly modified: CodeEditorWidget;
|
||||
public readonly original: CodeEditorWidget;
|
||||
public readonly original = this._register(this._createLeftHandSideEditor(this._options.editorOptions.get(), this._argCodeEditorWidgetOptions.originalEditor || {}));
|
||||
public readonly modified = this._register(this._createRightHandSideEditor(this._options.editorOptions.get(), this._argCodeEditorWidgetOptions.modifiedEditor || {}));
|
||||
|
||||
private readonly _onDidContentSizeChange = this._register(new Emitter<IContentSizeChangedEvent>());
|
||||
public get onDidContentSizeChange() { return this._onDidContentSizeChange.event; }
|
||||
|
||||
public readonly modifiedScrollTop: IObservable<number>;
|
||||
public readonly modifiedScrollHeight: IObservable<number>;
|
||||
public readonly modifiedScrollTop = observableFromEvent(this.modified.onDidScrollChange, () => /** @description modified.getScrollTop */ this.modified.getScrollTop());
|
||||
public readonly modifiedScrollHeight = observableFromEvent(this.modified.onDidScrollChange, () => /** @description modified.getScrollHeight */ this.modified.getScrollHeight());
|
||||
|
||||
public readonly modifiedModel: IObservable<ITextModel | null>;
|
||||
public readonly modifiedModel = observableFromEvent(this.modified.onDidChangeModel, () => /** @description modified.model */ this.modified.getModel());
|
||||
|
||||
public readonly modifiedSelections: IObservable<Selection[]>;
|
||||
public readonly modifiedCursor: IObservable<Position>;
|
||||
public readonly modifiedSelections = observableFromEvent(this.modified.onDidChangeCursorSelection, () => this.modified.getSelections() ?? []);
|
||||
public readonly modifiedCursor = derivedOpts({ owner: this, equalityComparer: Position.equals }, reader => this.modifiedSelections.read(reader)[0]?.getPosition() ?? new Position(1, 1));
|
||||
|
||||
public readonly originalCursor: IObservable<Position>;
|
||||
public readonly originalCursor = observableFromEvent(this.original.onDidChangeCursorPosition, () => this.original.getPosition() ?? new Position(1, 1));
|
||||
|
||||
public readonly isOriginalFocused = observableFromEvent(Event.any(this.original.onDidFocusEditorWidget, this.original.onDidBlurEditorWidget), () => this.original.hasWidgetFocus());
|
||||
public readonly isModifiedFocused = observableFromEvent(Event.any(this.modified.onDidFocusEditorWidget, this.modified.onDidBlurEditorWidget), () => this.modified.hasWidgetFocus());
|
||||
|
||||
public readonly isFocused = derived(this, reader => this.isOriginalFocused.read(reader) || this.isModifiedFocused.read(reader));
|
||||
|
||||
constructor(
|
||||
private readonly originalEditorElement: HTMLElement,
|
||||
private readonly modifiedEditorElement: HTMLElement,
|
||||
private readonly _options: DiffEditorOptions,
|
||||
codeEditorWidgetOptions: IDiffCodeEditorWidgetOptions,
|
||||
private _argCodeEditorWidgetOptions: IDiffCodeEditorWidgetOptions,
|
||||
private readonly _createInnerEditor: (instantiationService: IInstantiationService, container: HTMLElement, options: Readonly<IEditorOptions>, editorWidgetOptions: ICodeEditorWidgetOptions) => CodeEditorWidget,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IKeybindingService private readonly _keybindingService: IKeybindingService,
|
||||
@IKeybindingService private readonly _keybindingService: IKeybindingService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.original = this._register(this._createLeftHandSideEditor(_options.editorOptions.get(), codeEditorWidgetOptions.originalEditor || {}));
|
||||
this.modified = this._register(this._createRightHandSideEditor(_options.editorOptions.get(), codeEditorWidgetOptions.modifiedEditor || {}));
|
||||
|
||||
this.modifiedModel = observableFromEvent(this.modified.onDidChangeModel, () => /** @description modified.model */ this.modified.getModel());
|
||||
|
||||
this.modifiedScrollTop = observableFromEvent(this.modified.onDidScrollChange, () => /** @description modified.getScrollTop */ this.modified.getScrollTop());
|
||||
this.modifiedScrollHeight = observableFromEvent(this.modified.onDidScrollChange, () => /** @description modified.getScrollHeight */ this.modified.getScrollHeight());
|
||||
|
||||
this.modifiedSelections = observableFromEvent(this.modified.onDidChangeCursorSelection, () => this.modified.getSelections() ?? []);
|
||||
this.modifiedCursor = derivedOpts({ owner: this, equalityComparer: Position.equals }, reader => this.modifiedSelections.read(reader)[0]?.getPosition() ?? new Position(1, 1));
|
||||
|
||||
this.originalCursor = observableFromEvent(this.original.onDidChangeCursorPosition, () => this.original.getPosition() ?? new Position(1, 1));
|
||||
this._argCodeEditorWidgetOptions = null as any;
|
||||
|
||||
this._register(autorunHandleChanges({
|
||||
createEmptyChangeSummary: () => ({} as IDiffEditorConstructionOptions),
|
||||
|
|
|
@ -312,7 +312,7 @@ export class DiffEditorViewZones extends Disposable {
|
|||
}
|
||||
|
||||
let marginDomNode: HTMLElement | undefined = undefined;
|
||||
if (a.diff && a.diff.modified.isEmpty && this._options.shouldRenderRevertArrows.read(reader)) {
|
||||
if (a.diff && a.diff.modified.isEmpty && this._options.shouldRenderOldRevertArrows.read(reader)) {
|
||||
marginDomNode = createViewZoneMarginArrow();
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { AccessibleDiffViewerNext, AccessibleDiffViewerPrev, CollapseAllUnchangedRegions, ExitCompareMove, ShowAllUnchangedRegions, SwitchSide, ToggleCollapseUnchangedRegions, ToggleShowMovedCodeBlocks, ToggleUseInlineViewWhenSpaceIsLimited } from 'vs/editor/browser/widget/diffEditor/commands';
|
||||
import { AccessibleDiffViewerNext, AccessibleDiffViewerPrev, CollapseAllUnchangedRegions, ExitCompareMove, RevertHunkOrSelection, ShowAllUnchangedRegions, SwitchSide, ToggleCollapseUnchangedRegions, ToggleShowMovedCodeBlocks, ToggleUseInlineViewWhenSpaceIsLimited } from 'vs/editor/browser/widget/diffEditor/commands';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { localize } from 'vs/nls';
|
||||
import { MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions';
|
||||
|
@ -44,6 +44,35 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
|
|||
when: ContextKeyExpr.has('isInDiffEditor'),
|
||||
});
|
||||
|
||||
registerAction2(RevertHunkOrSelection);
|
||||
|
||||
for (const ctx of [
|
||||
{ icon: Codicon.arrowRight, key: EditorContextKeys.diffEditorInlineMode.toNegated() },
|
||||
{ icon: Codicon.discard, key: EditorContextKeys.diffEditorInlineMode }
|
||||
]) {
|
||||
MenuRegistry.appendMenuItem(MenuId.DiffEditorHunkToolbar, {
|
||||
command: {
|
||||
id: new RevertHunkOrSelection().desc.id,
|
||||
title: localize('revertHunk', "Revert Block"),
|
||||
icon: ctx.icon,
|
||||
},
|
||||
when: ContextKeyExpr.and(EditorContextKeys.diffEditorModifiedWritable, ctx.key),
|
||||
order: 5,
|
||||
group: 'primary',
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.DiffEditorSelectionToolbar, {
|
||||
command: {
|
||||
id: new RevertHunkOrSelection().desc.id,
|
||||
title: localize('revertSelection', "Revert Selection"),
|
||||
icon: ctx.icon,
|
||||
},
|
||||
when: ContextKeyExpr.and(EditorContextKeys.diffEditorModifiedWritable, ctx.key),
|
||||
order: 5,
|
||||
group: 'primary',
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
registerAction2(SwitchSide);
|
||||
registerAction2(ExitCompareMove);
|
||||
|
|
|
@ -32,12 +32,15 @@ export class DiffEditorOptions {
|
|||
);
|
||||
public readonly readOnly = derived(this, reader => this._options.read(reader).readOnly);
|
||||
|
||||
public readonly shouldRenderRevertArrows = derived(this, reader => {
|
||||
public readonly shouldRenderOldRevertArrows = derived(this, reader => {
|
||||
if (!this._options.read(reader).renderMarginRevertIcon) { return false; }
|
||||
if (!this.renderSideBySide.read(reader)) { return false; }
|
||||
if (this.readOnly.read(reader)) { return false; }
|
||||
if (this.shouldRenderGutterMenu.read(reader)) { return false; }
|
||||
return true;
|
||||
});
|
||||
|
||||
public readonly shouldRenderGutterMenu = derived(this, reader => this._options.read(reader).renderGutterMenu);
|
||||
public readonly renderIndicators = derived(this, reader => this._options.read(reader).renderIndicators);
|
||||
public readonly enableSplitViewResizing = derived(this, reader => this._options.read(reader).enableSplitViewResizing);
|
||||
public readonly splitViewDefaultRatio = derived(this, reader => this._options.read(reader).splitViewDefaultRatio);
|
||||
|
@ -99,5 +102,6 @@ function validateDiffEditorOptions(options: Readonly<IDiffEditorOptions>, defaul
|
|||
onlyShowAccessibleDiffViewer: validateBooleanOption(options.onlyShowAccessibleDiffViewer, defaults.onlyShowAccessibleDiffViewer),
|
||||
renderSideBySideInlineBreakpoint: clampedInt(options.renderSideBySideInlineBreakpoint, defaults.renderSideBySideInlineBreakpoint, 0, Constants.MAX_SAFE_SMALL_INTEGER),
|
||||
useInlineViewWhenSpaceIsLimited: validateBooleanOption(options.useInlineViewWhenSpaceIsLimited, defaults.useInlineViewWhenSpaceIsLimited),
|
||||
renderGutterMenu: validateBooleanOption(options.renderGutterMenu, defaults.renderGutterMenu),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ import { DiffEditorEditors } from './components/diffEditorEditors';
|
|||
import { DelegatingEditor } from './delegatingEditorImpl';
|
||||
import { DiffEditorOptions } from './diffEditorOptions';
|
||||
import { DiffEditorViewModel, DiffMapping, DiffState } from './diffEditorViewModel';
|
||||
import { DiffEditorGutter } from 'vs/editor/browser/widget/diffEditor/features/gutterFeature';
|
||||
|
||||
export interface IDiffCodeEditorWidgetOptions {
|
||||
originalEditor?: ICodeEditorWidgetOptions;
|
||||
|
@ -56,8 +57,8 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor {
|
|||
|
||||
private readonly elements = h('div.monaco-diff-editor.side-by-side', { style: { position: 'relative', height: '100%' } }, [
|
||||
h('div.noModificationsOverlay@overlay', { style: { position: 'absolute', height: '100%', visibility: 'hidden', } }, [$('span', {}, 'No Changes')]),
|
||||
h('div.editor.original@original', { style: { position: 'absolute', height: '100%' } }),
|
||||
h('div.editor.modified@modified', { style: { position: 'absolute', height: '100%' } }),
|
||||
h('div.editor.original@original', { style: { position: 'absolute', height: '100%', zIndex: '1', } }),
|
||||
h('div.editor.modified@modified', { style: { position: 'absolute', height: '100%', zIndex: '1', } }),
|
||||
h('div.accessibleDiffViewer@accessibleDiffViewer', { style: { position: 'absolute', height: '100%' } }),
|
||||
]);
|
||||
private readonly _diffModel = observableValue<DiffEditorViewModel | undefined>(this, undefined);
|
||||
|
@ -72,6 +73,9 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor {
|
|||
);
|
||||
private readonly _rootSizeObserver: ObservableElementSizeObserver;
|
||||
|
||||
/**
|
||||
* Is undefined if and only if side-by-side
|
||||
*/
|
||||
private readonly _sash: IObservable<DiffEditorSash | undefined>;
|
||||
private readonly _boundarySashes = observableValue<IBoundarySashes | undefined>(this, undefined);
|
||||
|
||||
|
@ -88,6 +92,8 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor {
|
|||
private readonly _overviewRulerPart: IObservable<OverviewRulerFeature | undefined>;
|
||||
private readonly _movedBlocksLinesPart = observableValue<MovedBlocksLinesFeature | undefined>(this, undefined);
|
||||
|
||||
private readonly _gutter: IObservable<DiffEditorGutter | undefined>;
|
||||
|
||||
public get collapseUnchangedRegions() { return this._options.hideUnchangedRegions.get(); }
|
||||
|
||||
constructor(
|
||||
|
@ -126,6 +132,9 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor {
|
|||
this._register(bindContextKey(EditorContextKeys.diffEditorRenderSideBySideInlineBreakpointReached, this._contextKeyService,
|
||||
reader => this._options.couldShowInlineViewBecauseOfSize.read(reader)
|
||||
));
|
||||
this._register(bindContextKey(EditorContextKeys.diffEditorInlineMode, this._contextKeyService,
|
||||
reader => !this._options.renderSideBySide.read(reader)
|
||||
));
|
||||
|
||||
this._register(bindContextKey(EditorContextKeys.hasChanges, this._contextKeyService,
|
||||
reader => (this._diffModel.read(reader)?.diff.read(reader)?.mappings.length ?? 0) > 0
|
||||
|
@ -140,6 +149,19 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor {
|
|||
(i, c, o, o2) => this._createInnerEditor(i, c, o, o2),
|
||||
));
|
||||
|
||||
this._register(bindContextKey(EditorContextKeys.diffEditorOriginalWritable, this._contextKeyService,
|
||||
reader => this._options.originalEditable.read(reader)
|
||||
));
|
||||
this._register(bindContextKey(EditorContextKeys.diffEditorModifiedWritable, this._contextKeyService,
|
||||
reader => !this._options.readOnly.read(reader)
|
||||
));
|
||||
this._register(bindContextKey(EditorContextKeys.diffEditorOriginalUri, this._contextKeyService,
|
||||
reader => this._diffModel.read(reader)?.model.original.uri.toString() ?? ''
|
||||
));
|
||||
this._register(bindContextKey(EditorContextKeys.diffEditorModifiedUri, this._contextKeyService,
|
||||
reader => this._diffModel.read(reader)?.model.modified.uri.toString() ?? ''
|
||||
));
|
||||
|
||||
this._overviewRulerPart = derivedDisposable(this, reader =>
|
||||
!this._options.renderOverviewRuler.read(reader)
|
||||
? undefined
|
||||
|
@ -245,6 +267,17 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor {
|
|||
|
||||
codeEditorService.addDiffEditor(this);
|
||||
|
||||
this._gutter = derivedDisposable(this, reader => {
|
||||
return this._options.shouldRenderGutterMenu.read(reader)
|
||||
? this._instantiationService.createInstance(
|
||||
readHotReloadableExport(DiffEditorGutter, reader),
|
||||
this.elements.root,
|
||||
this._diffModel,
|
||||
this._editors
|
||||
)
|
||||
: undefined;
|
||||
});
|
||||
|
||||
this._register(recomputeInitiallyAndOnChange(this._layoutInfo));
|
||||
|
||||
derivedDisposable(this, reader => /** @description MovedBlocksLinesPart */
|
||||
|
@ -290,7 +323,9 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor {
|
|||
}
|
||||
}));
|
||||
|
||||
this._register(new RevertButtonsFeature(this._editors, this._diffModel, this._options, this));
|
||||
this._register(autorunWithStore((reader, store) => {
|
||||
store.add(new (readHotReloadableExport(RevertButtonsFeature, reader))(this._editors, this._diffModel, this._options, this));
|
||||
}));
|
||||
}
|
||||
|
||||
public getViewWidth(): number {
|
||||
|
@ -307,23 +342,49 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor {
|
|||
}
|
||||
|
||||
private readonly _layoutInfo = derived(this, reader => {
|
||||
const width = this._rootSizeObserver.width.read(reader);
|
||||
const height = this._rootSizeObserver.height.read(reader);
|
||||
const sashLeft = this._sash.read(reader)?.sashLeft.read(reader);
|
||||
const fullWidth = this._rootSizeObserver.width.read(reader);
|
||||
const fullHeight = this._rootSizeObserver.height.read(reader);
|
||||
|
||||
const originalWidth = sashLeft ?? Math.max(5, this._editors.original.getLayoutInfo().decorationsLeft);
|
||||
const modifiedWidth = width - originalWidth - (this._overviewRulerPart.read(reader)?.width ?? 0);
|
||||
const sash = this._sash.read(reader);
|
||||
|
||||
const movedBlocksLinesWidth = this._movedBlocksLinesPart.read(reader)?.width.read(reader) ?? 0;
|
||||
const originalWidthWithoutMovedBlockLines = originalWidth - movedBlocksLinesWidth;
|
||||
this.elements.original.style.width = originalWidthWithoutMovedBlockLines + 'px';
|
||||
this.elements.original.style.left = '0px';
|
||||
const gutter = this._gutter.read(reader);
|
||||
const gutterWidth = gutter?.width.read(reader) ?? 0;
|
||||
|
||||
const overviewRulerPartWidth = this._overviewRulerPart.read(reader)?.width ?? 0;
|
||||
|
||||
let originalLeft: number, originalWidth: number, modifiedLeft: number, modifiedWidth: number, gutterLeft: number;
|
||||
|
||||
const sideBySide = !!sash;
|
||||
if (sideBySide) {
|
||||
const sashLeft = sash.sashLeft.read(reader);
|
||||
const movedBlocksLinesWidth = this._movedBlocksLinesPart.read(reader)?.width.read(reader) ?? 0;
|
||||
|
||||
originalLeft = 0;
|
||||
originalWidth = sashLeft - gutterWidth - movedBlocksLinesWidth;
|
||||
|
||||
gutterLeft = sashLeft - gutterWidth;
|
||||
|
||||
modifiedLeft = sashLeft;
|
||||
modifiedWidth = fullWidth - modifiedLeft - overviewRulerPartWidth;
|
||||
} else {
|
||||
gutterLeft = 0;
|
||||
|
||||
originalLeft = gutterWidth;
|
||||
originalWidth = Math.max(5, this._editors.original.getLayoutInfo().decorationsLeft);
|
||||
|
||||
modifiedLeft = gutterWidth + originalWidth;
|
||||
modifiedWidth = fullWidth - modifiedLeft - overviewRulerPartWidth;
|
||||
}
|
||||
|
||||
this.elements.original.style.left = originalLeft + 'px';
|
||||
this.elements.original.style.width = originalWidth + 'px';
|
||||
this._editors.original.layout({ width: originalWidth, height: fullHeight }, true);
|
||||
|
||||
gutter?.layout(gutterLeft);
|
||||
|
||||
this.elements.modified.style.left = modifiedLeft + 'px';
|
||||
this.elements.modified.style.width = modifiedWidth + 'px';
|
||||
this.elements.modified.style.left = originalWidth + 'px';
|
||||
|
||||
this._editors.original.layout({ width: originalWidthWithoutMovedBlockLines, height }, true);
|
||||
this._editors.modified.layout({ width: modifiedWidth, height }, true);
|
||||
this._editors.modified.layout({ width: modifiedWidth, height: fullHeight }, true);
|
||||
|
||||
return {
|
||||
modifiedEditor: this._editors.modified.getLayoutInfo(),
|
||||
|
|
|
@ -0,0 +1,276 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, h } from 'vs/base/browser/dom';
|
||||
import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IObservable, autorun, autorunWithStore, derived, observableFromEvent, observableValue } from 'vs/base/common/observable';
|
||||
import { DiffEditorEditors } from 'vs/editor/browser/widget/diffEditor/components/diffEditorEditors';
|
||||
import { DiffEditorViewModel } from 'vs/editor/browser/widget/diffEditor/diffEditorViewModel';
|
||||
import { appendRemoveOnDispose, applyStyle } from 'vs/editor/browser/widget/diffEditor/utils';
|
||||
import { EditorGutter, IGutterItemInfo, IGutterItemView } from 'vs/editor/browser/widget/diffEditor/utils/editorGutter';
|
||||
import { ActionRunnerWithContext } from 'vs/editor/browser/widget/multiDiffEditor/utils';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { LineRange, LineRangeSet } from 'vs/editor/common/core/lineRange';
|
||||
import { OffsetRange } from 'vs/editor/common/core/offsetRange';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { SingleTextEdit, TextEdit } from 'vs/editor/common/core/textEdit';
|
||||
import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping';
|
||||
import { TextModelText } from 'vs/editor/common/model/textModelText';
|
||||
import { HiddenItemStrategy, MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar';
|
||||
import { IMenuService, MenuId } from 'vs/platform/actions/common/actions';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
const emptyArr: never[] = [];
|
||||
const width = 35;
|
||||
|
||||
export class DiffEditorGutter extends Disposable {
|
||||
private readonly _menu = this._register(this._menuService.createMenu(MenuId.DiffEditorHunkToolbar, this._contextKeyService));
|
||||
private readonly _actions = observableFromEvent(this._menu.onDidChange, () => this._menu.getActions());
|
||||
private readonly _hasActions = this._actions.map(a => a.length > 0);
|
||||
|
||||
public readonly width = derived(this, reader => this._hasActions.read(reader) ? width : 0);
|
||||
|
||||
private readonly elements = h('div.gutter@gutter', { style: { position: 'absolute', height: '100%', width: width + 'px', zIndex: '0' } }, []);
|
||||
|
||||
constructor(
|
||||
diffEditorRoot: HTMLDivElement,
|
||||
private readonly _diffModel: IObservable<DiffEditorViewModel | undefined>,
|
||||
private readonly _editors: DiffEditorEditors,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
|
||||
@IMenuService private readonly _menuService: IMenuService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(appendRemoveOnDispose(diffEditorRoot, this.elements.root));
|
||||
|
||||
this._register(addDisposableListener(this.elements.root, 'click', () => {
|
||||
this._editors.modified.focus();
|
||||
}));
|
||||
|
||||
this._register(applyStyle(this.elements.root, { display: this._hasActions.map(a => a ? 'block' : 'none') }));
|
||||
|
||||
this._register(new EditorGutter<DiffGutterItem>(this._editors.modified, this.elements.root, {
|
||||
getIntersectingGutterItems: (range, reader) => {
|
||||
const model = this._diffModel.read(reader);
|
||||
if (!model) {
|
||||
return [];
|
||||
}
|
||||
const diffs = model.diff.read(reader);
|
||||
if (!diffs) { return []; }
|
||||
|
||||
const selection = this._selectedDiffs.read(reader);
|
||||
if (selection.length > 0) {
|
||||
const m = DetailedLineRangeMapping.fromRangeMappings(selection.flatMap(s => s.rangeMappings));
|
||||
return [new DiffGutterItem(m, true, MenuId.DiffEditorSelectionToolbar, undefined)];
|
||||
}
|
||||
|
||||
const currentDiff = this._currentDiff.read(reader);
|
||||
|
||||
return diffs.mappings.map(m => new DiffGutterItem(
|
||||
m.lineRangeMapping,
|
||||
m.lineRangeMapping === currentDiff?.lineRangeMapping,
|
||||
MenuId.DiffEditorHunkToolbar,
|
||||
undefined,
|
||||
));
|
||||
},
|
||||
createView: (item, target) => {
|
||||
return this._instantiationService.createInstance(DiffToolBar, item, target, this);
|
||||
},
|
||||
}));
|
||||
|
||||
this._register(addDisposableListener(this.elements.gutter, EventType.MOUSE_WHEEL, (e: IMouseWheelEvent) => {
|
||||
if (!this._editors.modified.getOption(EditorOption.scrollbar).handleMouseWheel) {
|
||||
this._editors.modified.delegateScrollFromMouseWheelEvent(e);
|
||||
}
|
||||
}, { passive: false }));
|
||||
}
|
||||
|
||||
public computeStagedValue(mapping: DetailedLineRangeMapping): string {
|
||||
const c = mapping.innerChanges ?? [];
|
||||
const edit = new TextEdit(c.map(c => new SingleTextEdit(c.originalRange, this._editors.modifiedModel.get()!.getValueInRange(c.modifiedRange))));
|
||||
const value = edit.apply(new TextModelText(this._editors.original.getModel()!));
|
||||
return value;
|
||||
}
|
||||
|
||||
private readonly _currentDiff = derived(this, (reader) => {
|
||||
const model = this._diffModel.read(reader);
|
||||
if (!model) {
|
||||
return undefined;
|
||||
}
|
||||
const mappings = model.diff.read(reader)?.mappings;
|
||||
|
||||
const cursorPosition = this._editors.modifiedCursor.read(reader);
|
||||
if (!cursorPosition) { return undefined; }
|
||||
|
||||
return mappings?.find(m => m.lineRangeMapping.modified.contains(cursorPosition.lineNumber));
|
||||
});
|
||||
|
||||
private readonly _selectedDiffs = derived(this, (reader) => {
|
||||
/** @description selectedDiffs */
|
||||
const model = this._diffModel.read(reader);
|
||||
const diff = model?.diff.read(reader);
|
||||
// Return `emptyArr` because it is a constant. [] is always a new array and would trigger a change.
|
||||
if (!diff) { return emptyArr; }
|
||||
|
||||
const selections = this._editors.modifiedSelections.read(reader);
|
||||
if (selections.every(s => s.isEmpty())) { return emptyArr; }
|
||||
|
||||
const selectedLineNumbers = new LineRangeSet(selections.map(s => LineRange.fromRangeInclusive(s)));
|
||||
|
||||
const selectedMappings = diff.mappings.filter(m =>
|
||||
m.lineRangeMapping.innerChanges && selectedLineNumbers.intersects(m.lineRangeMapping.modified)
|
||||
);
|
||||
const result = selectedMappings.map(mapping => ({
|
||||
mapping,
|
||||
rangeMappings: mapping.lineRangeMapping.innerChanges!.filter(
|
||||
c => selections.some(s => Range.areIntersecting(c.modifiedRange, s))
|
||||
)
|
||||
}));
|
||||
if (result.length === 0 || result.every(r => r.rangeMappings.length === 0)) { return emptyArr; }
|
||||
return result;
|
||||
});
|
||||
|
||||
layout(left: number) {
|
||||
this.elements.gutter.style.left = left + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
class DiffGutterItem implements IGutterItemInfo {
|
||||
constructor(
|
||||
public readonly mapping: DetailedLineRangeMapping,
|
||||
public readonly showAlways: boolean,
|
||||
public readonly menuId: MenuId,
|
||||
public readonly rangeOverride: LineRange | undefined,
|
||||
) {
|
||||
}
|
||||
get id(): string { return this.mapping.modified.toString(); }
|
||||
get range(): LineRange { return this.rangeOverride ?? this.mapping.modified; }
|
||||
}
|
||||
|
||||
|
||||
class DiffToolBar extends Disposable implements IGutterItemView {
|
||||
private readonly _elements = h('div.gutterItem', { style: { height: '20px', width: '34px' } }, [
|
||||
h('div.background@background', {}, []),
|
||||
h('div.buttons@buttons', {}, []),
|
||||
]);
|
||||
|
||||
private readonly _showAlways = this._item.map(this, item => item.showAlways);
|
||||
private readonly _menuId = this._item.map(this, item => item.menuId);
|
||||
|
||||
private readonly _isSmall = observableValue(this, false);
|
||||
|
||||
constructor(
|
||||
private readonly _item: IObservable<DiffGutterItem>,
|
||||
target: HTMLElement,
|
||||
gutter: DiffEditorGutter,
|
||||
@IInstantiationService instantiationService: IInstantiationService
|
||||
) {
|
||||
super();
|
||||
|
||||
//const r = new ObservableElementSizeObserver
|
||||
|
||||
const hoverDelegate = this._register(instantiationService.createInstance(
|
||||
WorkbenchHoverDelegate,
|
||||
'element',
|
||||
true,
|
||||
{ position: { hoverPosition: HoverPosition.RIGHT } }
|
||||
));
|
||||
|
||||
this._register(appendRemoveOnDispose(target, this._elements.root));
|
||||
|
||||
this._register(autorun(reader => {
|
||||
/** @description update showAlways */
|
||||
const showAlways = this._showAlways.read(reader);
|
||||
this._elements.root.classList.toggle('noTransition', true);
|
||||
this._elements.root.classList.toggle('showAlways', showAlways);
|
||||
setTimeout(() => {
|
||||
this._elements.root.classList.toggle('noTransition', false);
|
||||
}, 0);
|
||||
}));
|
||||
|
||||
|
||||
this._register(autorunWithStore((reader, store) => {
|
||||
this._elements.buttons.replaceChildren();
|
||||
const i = store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.buttons, this._menuId.read(reader), {
|
||||
orientation: ActionsOrientation.VERTICAL,
|
||||
hoverDelegate,
|
||||
toolbarOptions: {
|
||||
primaryGroup: g => g.startsWith('primary'),
|
||||
},
|
||||
overflowBehavior: { maxItems: this._isSmall.read(reader) ? 1 : 3 },
|
||||
hiddenItemStrategy: HiddenItemStrategy.Ignore,
|
||||
actionRunner: new ActionRunnerWithContext(() => {
|
||||
const mapping = this._item.get().mapping;
|
||||
return {
|
||||
mapping,
|
||||
originalWithModifiedChanges: gutter.computeStagedValue(mapping),
|
||||
} satisfies DiffEditorSelectionHunkToolbarContext;
|
||||
}),
|
||||
menuOptions: {
|
||||
shouldForwardArgs: true,
|
||||
},
|
||||
}));
|
||||
store.add(i.onDidChangeMenuItems(() => {
|
||||
if (this._lastItemRange) {
|
||||
this.layout(this._lastItemRange, this._lastViewRange!);
|
||||
}
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
private _lastItemRange: OffsetRange | undefined = undefined;
|
||||
private _lastViewRange: OffsetRange | undefined = undefined;
|
||||
|
||||
layout(itemRange: OffsetRange, viewRange: OffsetRange): void {
|
||||
this._lastItemRange = itemRange;
|
||||
this._lastViewRange = viewRange;
|
||||
|
||||
let itemHeight = this._elements.buttons.clientHeight;
|
||||
this._isSmall.set(this._item.get().mapping.original.startLineNumber === 1 && itemRange.length < 30, undefined);
|
||||
// Item might have changed
|
||||
itemHeight = this._elements.buttons.clientHeight;
|
||||
|
||||
this._elements.root.style.top = itemRange.start + 'px';
|
||||
this._elements.root.style.height = itemRange.length + 'px';
|
||||
|
||||
const middleHeight = itemRange.length / 2 - itemHeight / 2;
|
||||
|
||||
const margin = itemHeight;
|
||||
|
||||
let effectiveCheckboxTop = itemRange.start + middleHeight;
|
||||
|
||||
const preferredViewPortRange = OffsetRange.tryCreate(
|
||||
margin,
|
||||
viewRange.endExclusive - margin - itemHeight
|
||||
);
|
||||
|
||||
const preferredParentRange = OffsetRange.tryCreate(
|
||||
itemRange.start + margin,
|
||||
itemRange.endExclusive - itemHeight - margin
|
||||
);
|
||||
|
||||
if (preferredParentRange && preferredViewPortRange && preferredParentRange.start < preferredParentRange.endExclusive) {
|
||||
effectiveCheckboxTop = preferredViewPortRange!.clip(effectiveCheckboxTop);
|
||||
effectiveCheckboxTop = preferredParentRange!.clip(effectiveCheckboxTop);
|
||||
}
|
||||
|
||||
this._elements.buttons.style.top = `${effectiveCheckboxTop - itemRange.start}px`;
|
||||
}
|
||||
}
|
||||
|
||||
export interface DiffEditorSelectionHunkToolbarContext {
|
||||
mapping: DetailedLineRangeMapping;
|
||||
|
||||
/**
|
||||
* The original text with the selected modified changes applied.
|
||||
*/
|
||||
originalWithModifiedChanges: string;
|
||||
}
|
|
@ -31,7 +31,7 @@ export class RevertButtonsFeature extends Disposable {
|
|||
super();
|
||||
|
||||
this._register(autorunWithStore((reader, store) => {
|
||||
if (!this._options.shouldRenderRevertArrows.read(reader)) { return; }
|
||||
if (!this._options.shouldRenderOldRevertArrows.read(reader)) { return; }
|
||||
const model = this._diffModel.read(reader);
|
||||
const diff = model?.diff.read(reader);
|
||||
if (!model || !diff) { return; }
|
||||
|
|
|
@ -294,6 +294,11 @@
|
|||
border-left: 1px solid var(--vscode-diffEditor-border);
|
||||
}
|
||||
|
||||
.monaco-diff-editor.side-by-side .editor.original {
|
||||
box-shadow: 6px 0 5px -5px var(--vscode-scrollbar-shadow);
|
||||
border-right: 1px solid var(--vscode-diffEditor-border);
|
||||
}
|
||||
|
||||
.monaco-diff-editor .diffViewport {
|
||||
background: var(--vscode-scrollbarSlider-background);
|
||||
}
|
||||
|
@ -316,3 +321,76 @@
|
|||
);
|
||||
background-size: 8px 8px;
|
||||
}
|
||||
|
||||
.monaco-diff-editor .gutter {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
.gutterItem {
|
||||
opacity: 0;
|
||||
transition: opacity 0.7s;
|
||||
|
||||
&.showAlways {
|
||||
opacity: 1;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
&.noTransition {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .gutterItem {
|
||||
opacity: 1;
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.gutterItem {
|
||||
.background {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
left: 50%;
|
||||
width: 1px;
|
||||
|
||||
border-left: 2px rgb(214, 215, 229) solid;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
position: absolute;
|
||||
/*height: 100%;*/
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.monaco-toolbar {
|
||||
height: fit-content;
|
||||
.monaco-action-bar {
|
||||
line-height: 1;
|
||||
|
||||
.actions-container {
|
||||
width: fit-content;
|
||||
border-radius: 4px;
|
||||
border: 1px rgba(214, 215, 229, 0.644) solid;
|
||||
background: white;
|
||||
|
||||
.action-item {
|
||||
&:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.action-label {
|
||||
padding: 0.5px 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
164
src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts
Normal file
164
src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts
Normal file
|
@ -0,0 +1,164 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { autorun, IObservable, IReader, ISettableObservable, observableFromEvent, observableSignal, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable';
|
||||
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget';
|
||||
import { LineRange } from 'vs/editor/common/core/lineRange';
|
||||
import { OffsetRange } from 'vs/editor/common/core/offsetRange';
|
||||
|
||||
export class EditorGutter<T extends IGutterItemInfo = IGutterItemInfo> extends Disposable {
|
||||
private readonly scrollTop = observableFromEvent(
|
||||
this._editor.onDidScrollChange,
|
||||
(e) => /** @description editor.onDidScrollChange */ this._editor.getScrollTop()
|
||||
);
|
||||
private readonly isScrollTopZero = this.scrollTop.map((scrollTop) => /** @description isScrollTopZero */ scrollTop === 0);
|
||||
private readonly modelAttached = observableFromEvent(
|
||||
this._editor.onDidChangeModel,
|
||||
(e) => /** @description editor.onDidChangeModel */ this._editor.hasModel()
|
||||
);
|
||||
|
||||
private readonly editorOnDidChangeViewZones = observableSignalFromEvent('onDidChangeViewZones', this._editor.onDidChangeViewZones);
|
||||
private readonly editorOnDidContentSizeChange = observableSignalFromEvent('onDidContentSizeChange', this._editor.onDidContentSizeChange);
|
||||
private readonly domNodeSizeChanged = observableSignal('domNodeSizeChanged');
|
||||
|
||||
constructor(
|
||||
private readonly _editor: CodeEditorWidget,
|
||||
private readonly _domNode: HTMLElement,
|
||||
private readonly itemProvider: IGutterItemProvider<T>
|
||||
) {
|
||||
super();
|
||||
this._domNode.className = 'gutter monaco-editor';
|
||||
const scrollDecoration = this._domNode.appendChild(
|
||||
h('div.scroll-decoration', { role: 'presentation', ariaHidden: 'true', style: { width: '100%' } })
|
||||
.root
|
||||
);
|
||||
|
||||
const o = new ResizeObserver(() => {
|
||||
transaction(tx => {
|
||||
/** @description ResizeObserver: size changed */
|
||||
this.domNodeSizeChanged.trigger(tx);
|
||||
});
|
||||
});
|
||||
o.observe(this._domNode);
|
||||
this._register(toDisposable(() => o.disconnect()));
|
||||
|
||||
this._register(autorun(reader => {
|
||||
/** @description update scroll decoration */
|
||||
scrollDecoration.className = this.isScrollTopZero.read(reader) ? '' : 'scroll-decoration';
|
||||
}));
|
||||
|
||||
this._register(autorun(reader => /** @description EditorGutter.Render */ this.render(reader)));
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
reset(this._domNode);
|
||||
}
|
||||
|
||||
private readonly views = new Map<string, ManagedGutterItemView>();
|
||||
|
||||
private render(reader: IReader): void {
|
||||
if (!this.modelAttached.read(reader)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.domNodeSizeChanged.read(reader);
|
||||
this.editorOnDidChangeViewZones.read(reader);
|
||||
this.editorOnDidContentSizeChange.read(reader);
|
||||
|
||||
const scrollTop = this.scrollTop.read(reader);
|
||||
|
||||
const visibleRanges = this._editor.getVisibleRanges();
|
||||
const unusedIds = new Set(this.views.keys());
|
||||
|
||||
const viewRange = OffsetRange.ofStartAndLength(0, this._domNode.clientHeight);
|
||||
|
||||
if (!viewRange.isEmpty) {
|
||||
for (const visibleRange of visibleRanges) {
|
||||
const visibleRange2 = new LineRange(
|
||||
visibleRange.startLineNumber,
|
||||
visibleRange.endLineNumber + 1
|
||||
);
|
||||
|
||||
const gutterItems = this.itemProvider.getIntersectingGutterItems(
|
||||
visibleRange2,
|
||||
reader
|
||||
);
|
||||
|
||||
transaction(tx => {
|
||||
/** EditorGutter.render */
|
||||
|
||||
for (const gutterItem of gutterItems) {
|
||||
if (!gutterItem.range.intersect(visibleRange2)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unusedIds.delete(gutterItem.id);
|
||||
let view = this.views.get(gutterItem.id);
|
||||
if (!view) {
|
||||
const viewDomNode = document.createElement('div');
|
||||
this._domNode.appendChild(viewDomNode);
|
||||
const gutterItemObs = observableValue('item', gutterItem);
|
||||
const itemView = this.itemProvider.createView(
|
||||
gutterItemObs,
|
||||
viewDomNode
|
||||
);
|
||||
view = new ManagedGutterItemView(gutterItemObs, itemView, viewDomNode);
|
||||
this.views.set(gutterItem.id, view);
|
||||
} else {
|
||||
view.item.set(gutterItem, tx);
|
||||
}
|
||||
|
||||
const top =
|
||||
gutterItem.range.startLineNumber <= this._editor.getModel()!.getLineCount()
|
||||
? this._editor.getTopForLineNumber(gutterItem.range.startLineNumber, true) - scrollTop
|
||||
: this._editor.getBottomForLineNumber(gutterItem.range.startLineNumber - 1, false) - scrollTop;
|
||||
const bottom = this._editor.getBottomForLineNumber(gutterItem.range.endLineNumberExclusive - 1, true) - scrollTop;
|
||||
|
||||
const height = bottom - top;
|
||||
|
||||
view.domNode.style.top = `${top}px`;
|
||||
view.domNode.style.height = `${height}px`;
|
||||
|
||||
view.gutterItemView.layout(OffsetRange.ofStartAndLength(top, height), viewRange);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of unusedIds) {
|
||||
const view = this.views.get(id)!;
|
||||
view.gutterItemView.dispose();
|
||||
this._domNode.removeChild(view.domNode);
|
||||
this.views.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ManagedGutterItemView {
|
||||
constructor(
|
||||
public readonly item: ISettableObservable<IGutterItemInfo>,
|
||||
public readonly gutterItemView: IGutterItemView,
|
||||
public readonly domNode: HTMLDivElement,
|
||||
) { }
|
||||
}
|
||||
|
||||
export interface IGutterItemProvider<TItem extends IGutterItemInfo> {
|
||||
getIntersectingGutterItems(range: LineRange, reader: IReader): TItem[];
|
||||
|
||||
createView(item: IObservable<TItem>, target: HTMLElement): IGutterItemView;
|
||||
}
|
||||
|
||||
export interface IGutterItemInfo {
|
||||
id: string;
|
||||
range: LineRange;
|
||||
}
|
||||
|
||||
export interface IGutterItemView extends IDisposable {
|
||||
layout(itemRange: OffsetRange, viewRange: OffsetRange): void;
|
||||
}
|
|
@ -6,11 +6,12 @@
|
|||
import { ActionRunner, IAction } from 'vs/base/common/actions';
|
||||
|
||||
export class ActionRunnerWithContext extends ActionRunner {
|
||||
constructor(private readonly _getContext: () => any) {
|
||||
constructor(private readonly _getContext: () => unknown) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected override runAction(action: IAction, _context?: unknown): Promise<void> {
|
||||
return super.runAction(action, this._getContext());
|
||||
const ctx = this._getContext();
|
||||
return super.runAction(action, ctx);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ export const diffEditorDefaultOptions = {
|
|||
splitViewDefaultRatio: 0.5,
|
||||
renderSideBySide: true,
|
||||
renderMarginRevertIcon: true,
|
||||
renderGutterMenu: true,
|
||||
maxComputationTime: 5000,
|
||||
maxFileSize: 50,
|
||||
ignoreTrimWhitespace: true,
|
||||
|
|
|
@ -175,6 +175,11 @@ const editorConfiguration: IConfigurationNode = {
|
|||
default: diffEditorDefaultOptions.renderMarginRevertIcon,
|
||||
description: nls.localize('renderMarginRevertIcon', "When enabled, the diff editor shows arrows in its glyph margin to revert changes.")
|
||||
},
|
||||
'diffEditor.renderGutterMenu': {
|
||||
type: 'boolean',
|
||||
default: diffEditorDefaultOptions.renderGutterMenu,
|
||||
description: nls.localize('renderGutterMenu', "When enabled, the diff editor shows a special gutter for revert and stage actions.")
|
||||
},
|
||||
'diffEditor.ignoreTrimWhitespace': {
|
||||
type: 'boolean',
|
||||
default: diffEditorDefaultOptions.ignoreTrimWhitespace,
|
||||
|
|
|
@ -803,6 +803,10 @@ export interface IDiffEditorBaseOptions {
|
|||
* Default to true.
|
||||
*/
|
||||
renderMarginRevertIcon?: boolean;
|
||||
/**
|
||||
* Indicates if the gutter menu should be rendered.
|
||||
*/
|
||||
renderGutterMenu?: boolean;
|
||||
/**
|
||||
* Original model should be editable?
|
||||
* Defaults to false.
|
||||
|
|
|
@ -52,6 +52,19 @@ export class LineRange {
|
|||
return result.ranges;
|
||||
}
|
||||
|
||||
public static join(lineRanges: LineRange[]): LineRange {
|
||||
if (lineRanges.length === 0) {
|
||||
throw new BugIndicatingError('lineRanges cannot be empty');
|
||||
}
|
||||
let startLineNumber = lineRanges[0].startLineNumber;
|
||||
let endLineNumberExclusive = lineRanges[0].endLineNumberExclusive;
|
||||
for (let i = 1; i < lineRanges.length; i++) {
|
||||
startLineNumber = Math.min(startLineNumber, lineRanges[i].startLineNumber);
|
||||
endLineNumberExclusive = Math.max(endLineNumberExclusive, lineRanges[i].endLineNumberExclusive);
|
||||
}
|
||||
return new LineRange(startLineNumber, endLineNumberExclusive);
|
||||
}
|
||||
|
||||
public static ofLength(startLineNumber: number, length: number): LineRange {
|
||||
return new LineRange(startLineNumber, startLineNumber + length);
|
||||
}
|
||||
|
|
|
@ -92,6 +92,12 @@ export class LineRangeMapping {
|
|||
* Also contains inner range mappings.
|
||||
*/
|
||||
export class DetailedLineRangeMapping extends LineRangeMapping {
|
||||
public static fromRangeMappings(rangeMappings: RangeMapping[]): DetailedLineRangeMapping {
|
||||
const originalRange = LineRange.join(rangeMappings.map(r => LineRange.fromRangeInclusive(r.originalRange)));
|
||||
const modifiedRange = LineRange.join(rangeMappings.map(r => LineRange.fromRangeInclusive(r.modifiedRange)));
|
||||
return new DetailedLineRangeMapping(originalRange, modifiedRange, rangeMappings);
|
||||
}
|
||||
|
||||
/**
|
||||
* If inner changes have not been computed, this is set to undefined.
|
||||
* Otherwise, it represents the character-level diff in this line range.
|
||||
|
|
|
@ -30,10 +30,16 @@ export namespace EditorContextKeys {
|
|||
export const inMultiDiffEditor = new RawContextKey<boolean>('inMultiDiffEditor', false, nls.localize('inMultiDiffEditor', "Whether the context is a multi diff editor"));
|
||||
export const multiDiffEditorAllCollapsed = new RawContextKey<boolean>('multiDiffEditorAllCollapsed', undefined, nls.localize('multiDiffEditorAllCollapsed', "Whether all files in multi diff editor are collapsed"));
|
||||
export const hasChanges = new RawContextKey<boolean>('diffEditorHasChanges', false, nls.localize('diffEditorHasChanges', "Whether the diff editor has changes"));
|
||||
|
||||
export const comparingMovedCode = new RawContextKey<boolean>('comparingMovedCode', false, nls.localize('comparingMovedCode', "Whether a moved code block is selected for comparison"));
|
||||
export const accessibleDiffViewerVisible = new RawContextKey<boolean>('accessibleDiffViewerVisible', false, nls.localize('accessibleDiffViewerVisible', "Whether the accessible diff viewer is visible"));
|
||||
export const diffEditorRenderSideBySideInlineBreakpointReached = new RawContextKey<boolean>('diffEditorRenderSideBySideInlineBreakpointReached', false, nls.localize('diffEditorRenderSideBySideInlineBreakpointReached', "Whether the diff editor render side by side inline breakpoint is reached"));
|
||||
export const diffEditorInlineMode = new RawContextKey<boolean>('diffEditorInlineMode', false, nls.localize('diffEditorInlineMode', "Whether inline mode is active"));
|
||||
|
||||
export const diffEditorOriginalWritable = new RawContextKey<boolean>('diffEditorOriginalWritable', false, nls.localize('diffEditorOriginalWritable', "Whether modified is writable in the diff editor"));
|
||||
export const diffEditorModifiedWritable = new RawContextKey<boolean>('diffEditorModifiedWritable', false, nls.localize('diffEditorModifiedWritable', "Whether modified is writable in the diff editor"));
|
||||
export const diffEditorOriginalUri = new RawContextKey<string>('diffEditorOriginalUri', '', nls.localize('diffEditorOriginalUri', "The uri of the original document"));
|
||||
export const diffEditorModifiedUri = new RawContextKey<string>('diffEditorModifiedUri', '', nls.localize('diffEditorModifiedUri', "The uri of the modified document"));
|
||||
|
||||
export const columnSelection = new RawContextKey<boolean>('editorColumnSelection', false, nls.localize('editorColumnSelection', "Whether `editor.columnSelection` is enabled"));
|
||||
export const writable = readOnly.toNegated();
|
||||
export const hasNonEmptySelection = new RawContextKey<boolean>('editorHasSelection', false, nls.localize('editorHasSelection', "Whether the editor has text selected"));
|
||||
|
|
4
src/vs/monaco.d.ts
vendored
4
src/vs/monaco.d.ts
vendored
|
@ -3818,6 +3818,10 @@ declare namespace monaco.editor {
|
|||
* Default to true.
|
||||
*/
|
||||
renderMarginRevertIcon?: boolean;
|
||||
/**
|
||||
* Indicates if the gutter menu should be rendered.
|
||||
*/
|
||||
renderGutterMenu?: boolean;
|
||||
/**
|
||||
* Original model should be editable?
|
||||
* Defaults to false.
|
||||
|
|
|
@ -220,6 +220,9 @@ export class MenuId {
|
|||
static readonly ChatInputSide = new MenuId('ChatInputSide');
|
||||
static readonly AccessibleView = new MenuId('AccessibleView');
|
||||
static readonly MultiDiffEditorFileToolbar = new MenuId('MultiDiffEditorFileToolbar');
|
||||
static readonly DiffEditorHunkToolbar = new MenuId('DiffEditorHunkToolbar');
|
||||
static readonly DiffEditorSelectionToolbar = new MenuId('DiffEditorSelectionToolbar');
|
||||
|
||||
|
||||
/**
|
||||
* Create or reuse a `MenuId` with the given identifier
|
||||
|
|
|
@ -441,6 +441,18 @@ const apiMenus: IAPIMenu[] = [
|
|||
id: MenuId.MultiDiffEditorFileToolbar,
|
||||
description: localize('menus.multiDiffEditorResource', "The resource toolbar in the multi diff editor"),
|
||||
proposed: 'contribMultiDiffEditorMenus'
|
||||
},
|
||||
{
|
||||
key: 'diffEditor/gutter/hunk',
|
||||
id: MenuId.DiffEditorHunkToolbar,
|
||||
description: localize('menus.diffEditorGutterToolBarMenus', "The gutter toolbar in the diff editor"),
|
||||
proposed: 'contribDiffEditorGutterToolBarMenus'
|
||||
},
|
||||
{
|
||||
key: 'diffEditor/gutter/selection',
|
||||
id: MenuId.DiffEditorSelectionToolbar,
|
||||
description: localize('menus.diffEditorGutterToolBarMenus', "The gutter toolbar in the diff editor"),
|
||||
proposed: 'contribDiffEditorGutterToolBarMenus'
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ export const allApiProposals = Object.freeze({
|
|||
contribCommentPeekContext: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentPeekContext.d.ts',
|
||||
contribCommentThreadAdditionalMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentThreadAdditionalMenu.d.ts',
|
||||
contribCommentsViewThreadMenus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentsViewThreadMenus.d.ts',
|
||||
contribDiffEditorGutterToolBarMenus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribDiffEditorGutterToolBarMenus.d.ts',
|
||||
contribEditSessions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditSessions.d.ts',
|
||||
contribEditorContentMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditorContentMenu.d.ts',
|
||||
contribIssueReporter: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribIssueReporter.d.ts',
|
||||
|
|
6
src/vscode-dts/vscode.proposed.contribDiffEditorGutterToolBarMenus.d.ts
vendored
Normal file
6
src/vscode-dts/vscode.proposed.contribDiffEditorGutterToolBarMenus.d.ts
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// empty placeholder declaration for `diffEditor/gutter/*` menus
|
Loading…
Reference in a new issue