From 4dd7a526cd620e557f6539cb4468e279fafea60f Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Mon, 12 Jun 2023 15:07:14 +0200 Subject: [PATCH] Implements diff editor revert button. (#184909) --- .../widget/diffEditorWidget2/decorations.ts | 8 +++ .../diffEditorWidget2/diffEditorWidget2.ts | 62 ++++++++++++++----- .../inlineDiffDeletedCodeMargin.ts | 51 ++++++++------- .../widget/diffEditorWidget2/lineAlignment.ts | 19 +++++- 4 files changed, 99 insertions(+), 41 deletions(-) diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/decorations.ts b/src/vs/editor/browser/widget/diffEditorWidget2/decorations.ts index 4d6dcafe9551e..717c8b5e14d03 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/decorations.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/decorations.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from 'vs/base/common/codicons'; +import { MarkdownString } from 'vs/base/common/htmlContent'; import { ThemeIcon } from 'vs/base/common/themables'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { localize } from 'vs/nls'; @@ -37,3 +38,10 @@ export const diffDeleteDecoration = ModelDecorationOptions.register({ className: 'char-delete', description: 'char-delete', }); + +export const arrowRevertChange = ModelDecorationOptions.register({ + description: 'diff-editor-arrow-revert-change', + glyphMarginHoverMessage: new MarkdownString(undefined, { isTrusted: true, supportThemeIcons: true }).appendMarkdown(localize('revertChangeHoverMessage', 'Click to revert change')), + glyphMarginClassName: 'arrow-revert-change ' + ThemeIcon.asClassName(Codicon.arrowRight), + zIndex: 10001, +}); diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts index dc355e41d60e2..74346212f94c8 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts @@ -14,12 +14,12 @@ import { isDefined } from 'vs/base/common/types'; import { Constants } from 'vs/base/common/uint'; import 'vs/css!./style'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; -import { ICodeEditor, IDiffEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IDiffEditor, IDiffEditorConstructionOptions, IMouseTargetViewZone } 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 { arrowRevertChange, diffAddDecoration, diffDeleteDecoration, diffFullLineAddDecoration, diffFullLineDeleteDecoration } from 'vs/editor/browser/widget/diffEditorWidget2/decorations'; import { DiffEditorSash } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorSash'; import { ViewZoneManager } from 'vs/editor/browser/widget/diffEditorWidget2/lineAlignment'; import { MovedBlocksLinesPart } from 'vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines'; @@ -40,6 +40,8 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { DelegatingEditor } from './delegatingEditorImpl'; import { DiffMapping, DiffModel } from './diffModel'; +import { Range } from 'vs/editor/common/core/range'; +import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; const diffEditorDefaultOptions: ValidDiffEditorBaseOptions = { enableSplitViewResizing: true, @@ -142,7 +144,16 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { this._register(keepAlive(this._sash, true)); this._register(new UnchangedRangesFeature(this._originalEditor, this._modifiedEditor, this._diffModel)); - this._register(this._instantiationService.createInstance(ViewZoneManager, this._originalEditor, this._modifiedEditor, this._diffModel, this._options.map(o => o.renderSideBySide))); + this._register( + this._instantiationService.createInstance( + ViewZoneManager, + this._originalEditor, + this._modifiedEditor, + this._diffModel, + this._options.map((o) => o.renderSideBySide), + this + ) + ); this._register(this._instantiationService.createInstance(OverviewRulerPart, this._originalEditor, @@ -229,6 +240,10 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { originalDecorations.push({ range: i.originalRange, options: diffDeleteDecoration }); modifiedDecorations.push({ range: i.modifiedRange, options: diffAddDecoration }); } + + if (!m.lineRangeMapping.modifiedRange.isEmpty) { + modifiedDecorations.push({ range: Range.fromPositions(new Position(m.lineRangeMapping.modifiedRange.startLineNumber, 1)), options: arrowRevertChange }); + } } if (currentMove) { @@ -311,24 +326,32 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { m.syncedMovedTexts.set(movedText, undefined); })); // 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); + const viewZone = event.target as IMouseTargetViewZone | undefined; + + const model = this._diffModel.get(); + if (!model) { + return; + } + const diffs = model.diff.get()?.mappings; + if (!diffs) { + return; + } + const diff = diffs.find(d => + viewZone?.detail.afterLineNumber === d.lineRangeMapping.modifiedRange.startLineNumber - 1 || + d.lineRangeMapping.modifiedRange.startLineNumber === lineNumber + ); + if (!diff) { + return; } + this.revert(diff.lineRangeMapping); + event.event.stopPropagation(); - this._updateDecorations(); return; } - }));*/ + })); return editor; } @@ -579,6 +602,17 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { }; } + public revert(diff: LineRangeMapping): void { + const model = this._model.get(); + if (!model) { + return; + } + const originalText = model.original.getValueInRange(diff.originalRange.toExclusiveRange()); + this._modifiedEditor.executeEdits('diffEditor', [ + { range: diff.modifiedRange.toExclusiveRange(), text: originalText } + ]); + } + private _goTo(diff: DiffMapping): void { this._modifiedEditor.setPosition(new Position(diff.lineRangeMapping.modifiedRange.startLineNumber, 1)); this._modifiedEditor.revealRangeInCenter(diff.lineRangeMapping.modifiedRange.toExclusiveRange()); diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/inlineDiffDeletedCodeMargin.ts b/src/vs/editor/browser/widget/diffEditorWidget2/inlineDiffDeletedCodeMargin.ts index e31ed0d4d8751..da0b9ff8d17f8 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/inlineDiffDeletedCodeMargin.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/inlineDiffDeletedCodeMargin.ts @@ -11,6 +11,7 @@ import { isIOS } from 'vs/base/common/platform'; import { ThemeIcon } from 'vs/base/common/themables'; import { IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { DiffEditorWidget2 } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; import { EndOfLineSequence, ITextModel } from 'vs/editor/common/model'; @@ -42,9 +43,10 @@ export class InlineDiffDeletedCodeMargin extends Disposable { constructor( private readonly _getViewZoneId: () => string, private readonly _marginDomNode: HTMLElement, - private readonly editor: CodeEditorWidget, - private readonly diff: LineRangeMapping, - private readonly viewLineCounts: number[], + private readonly _modifiedEditor: CodeEditorWidget, + private readonly _diff: LineRangeMapping, + private readonly _editor: DiffEditorWidget2, + private readonly _viewLineCounts: number[], private readonly _originalTextModel: ITextModel, private readonly _contextMenuService: IContextMenuService, private readonly _clipboardService: IClipboardService, @@ -57,7 +59,7 @@ export class InlineDiffDeletedCodeMargin extends Disposable { this._diffActions = document.createElement('div'); this._diffActions.className = ThemeIcon.asClassName(Codicon.lightBulb) + ' lightbulb-glyph'; this._diffActions.style.position = 'absolute'; - const lineHeight = this.editor.getOption(EditorOption.lineHeight); + const lineHeight = this._modifiedEditor.getOption(EditorOption.lineHeight); this._diffActions.style.right = '0px'; this._diffActions.style.visibility = 'hidden'; this._diffActions.style.height = `${lineHeight}px`; @@ -65,38 +67,38 @@ export class InlineDiffDeletedCodeMargin extends Disposable { this._marginDomNode.appendChild(this._diffActions); const actions: Action[] = []; - const isDeletion = diff.modifiedRange.isEmpty; + const isDeletion = _diff.modifiedRange.isEmpty; // default action actions.push(new Action( 'diff.clipboard.copyDeletedContent', isDeletion - ? (diff.originalRange.length > 1 + ? (_diff.originalRange.length > 1 ? localize('diff.clipboard.copyDeletedLinesContent.label', "Copy deleted lines") : localize('diff.clipboard.copyDeletedLinesContent.single.label', "Copy deleted line")) - : (diff.originalRange.length > 1 + : (_diff.originalRange.length > 1 ? localize('diff.clipboard.copyChangedLinesContent.label', "Copy changed lines") : localize('diff.clipboard.copyChangedLinesContent.single.label', "Copy changed line")), undefined, true, async () => { - const originalText = this._originalTextModel.getValueInRange(diff.originalRange.toExclusiveRange()); + const originalText = this._originalTextModel.getValueInRange(_diff.originalRange.toExclusiveRange()); await this._clipboardService.writeText(originalText); } )); let currentLineNumberOffset = 0; let copyLineAction: Action | undefined = undefined; - if (diff.originalRange.length > 1) { + if (_diff.originalRange.length > 1) { copyLineAction = new Action( 'diff.clipboard.copyDeletedLineContent', isDeletion - ? localize('diff.clipboard.copyDeletedLineContent.label', "Copy deleted line ({0})", diff.originalRange.startLineNumber) - : localize('diff.clipboard.copyChangedLineContent.label', "Copy changed line ({0})", diff.originalRange.startLineNumber), + ? localize('diff.clipboard.copyDeletedLineContent.label', "Copy deleted line ({0})", _diff.originalRange.startLineNumber) + : localize('diff.clipboard.copyChangedLineContent.label', "Copy changed line ({0})", _diff.originalRange.startLineNumber), undefined, true, async () => { - let lineContent = this._originalTextModel.getLineContent(diff.originalRange.startLineNumber + currentLineNumberOffset); + let lineContent = this._originalTextModel.getLineContent(_diff.originalRange.startLineNumber + currentLineNumberOffset); if (lineContent === '') { // empty line -> new line const eof = this._originalTextModel.getEndOfLineSequence(); @@ -109,21 +111,18 @@ export class InlineDiffDeletedCodeMargin extends Disposable { actions.push(copyLineAction); } - const readOnly = editor.getOption(EditorOption.readOnly); + const readOnly = _modifiedEditor.getOption(EditorOption.readOnly); if (!readOnly) { actions.push(new Action('diff.inline.revertChange', localize('diff.inline.revertChange.label', "Revert this change"), undefined, true, async () => { - const originalText = this._originalTextModel.getValueInRange(this.diff.originalRange.toExclusiveRange()); - editor.executeEdits('diffEditor', [ - { range: this.diff.modifiedRange.toExclusiveRange(), text: originalText } - ]); + this._editor.revert(this._diff); })); } - const useShadowDOM = editor.getOption(EditorOption.useShadowDOM) && !isIOS; // Do not use shadow dom on IOS #122035 + const useShadowDOM = _modifiedEditor.getOption(EditorOption.useShadowDOM) && !isIOS; // Do not use shadow dom on IOS #122035 const showContextMenu = (x: number, y: number) => { this._contextMenuService.showContextMenu({ - domForShadowRoot: useShadowDOM ? editor.getDomNode() ?? undefined : undefined, + domForShadowRoot: useShadowDOM ? _modifiedEditor.getDomNode() ?? undefined : undefined, getAnchor: () => { return { x, @@ -134,8 +133,8 @@ export class InlineDiffDeletedCodeMargin extends Disposable { if (copyLineAction) { copyLineAction.label = isDeletion - ? localize('diff.clipboard.copyDeletedLineContent.label', "Copy deleted line ({0})", diff.originalRange.startLineNumber + currentLineNumberOffset) - : localize('diff.clipboard.copyChangedLineContent.label', "Copy changed line ({0})", diff.originalRange.startLineNumber + currentLineNumberOffset); + ? localize('diff.clipboard.copyDeletedLineContent.label', "Copy deleted line ({0})", _diff.originalRange.startLineNumber + currentLineNumberOffset) + : localize('diff.clipboard.copyChangedLineContent.label', "Copy changed line ({0})", _diff.originalRange.startLineNumber + currentLineNumberOffset); } return actions; }, @@ -150,7 +149,7 @@ export class InlineDiffDeletedCodeMargin extends Disposable { showContextMenu(e.posx, top + height + pad); })); - this._register(editor.onMouseMove((e: IEditorMouseEvent) => { + this._register(_modifiedEditor.onMouseMove((e: IEditorMouseEvent) => { if ((e.target.type === MouseTargetType.CONTENT_VIEW_ZONE || e.target.type === MouseTargetType.GUTTER_VIEW_ZONE) && e.target.detail.viewZoneId === this._getViewZoneId()) { currentLineNumberOffset = this._updateLightBulbPosition(this._marginDomNode, e.event.browserEvent.y, lineHeight); this.visibility = true; @@ -159,7 +158,7 @@ export class InlineDiffDeletedCodeMargin extends Disposable { } })); - this._register(editor.onMouseDown((e: IEditorMouseEvent) => { + this._register(_modifiedEditor.onMouseDown((e: IEditorMouseEvent) => { if (!e.event.rightButton) { return; } @@ -182,10 +181,10 @@ export class InlineDiffDeletedCodeMargin extends Disposable { const lineNumberOffset = Math.floor(offset / lineHeight); const newTop = lineNumberOffset * lineHeight; this._diffActions.style.top = `${newTop}px`; - if (this.viewLineCounts) { + if (this._viewLineCounts) { let acc = 0; - for (let i = 0; i < this.viewLineCounts.length; i++) { - acc += this.viewLineCounts[i]; + for (let i = 0; i < this._viewLineCounts.length; i++) { + acc += this._viewLineCounts[i]; if (lineNumberOffset < acc) { return i; } diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts b/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts index 98d93ee816110..7208098e3744e 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts @@ -3,7 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { $ } from 'vs/base/browser/dom'; import { ArrayQueue } from 'vs/base/common/arrays'; +import { Codicon } from 'vs/base/common/codicons'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IObservable, derived, observableFromEvent, observableSignalFromEvent, observableValue } from 'vs/base/common/observable'; import { autorun, autorunWithStore2 } from 'vs/base/common/observableImpl/autorun'; @@ -14,6 +16,7 @@ import { IViewZone } from 'vs/editor/browser/editorBrowser'; import { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { diffDeleteDecoration, diffRemoveIcon } from 'vs/editor/browser/widget/diffEditorWidget2/decorations'; +import { DiffEditorWidget2 } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2'; import { DiffMapping, DiffModel } from 'vs/editor/browser/widget/diffEditorWidget2/diffModel'; import { InlineDiffDeletedCodeMargin } from 'vs/editor/browser/widget/diffEditorWidget2/inlineDiffDeletedCodeMargin'; import { LineSource, RenderOptions, renderLines } from 'vs/editor/browser/widget/diffEditorWidget2/renderLines'; @@ -49,6 +52,7 @@ export class ViewZoneManager extends Disposable { private readonly _modifiedEditor: CodeEditorWidget, private readonly _diffModel: IObservable, private readonly _renderSideBySide: IObservable, + private readonly _diffEditorWidget: DiffEditorWidget2, @IClipboardService private readonly _clipboardService: IClipboardService, @IContextMenuService private readonly _contextMenuService: IContextMenuService, ) { @@ -190,10 +194,11 @@ export class ViewZoneManager extends Disposable { marginDomNode, this._modifiedEditor, a.diff, + this._diffEditorWidget, result.viewLineCounts, this._originalEditor.getModel()!, this._contextMenuService, - this._clipboardService + this._clipboardService, ) ); @@ -245,10 +250,22 @@ export class ViewZoneManager extends Disposable { continue; } + function createViewZoneMarginArrow(): HTMLElement { + const arrow = document.createElement('div'); + arrow.className = 'arrow-revert-change ' + ThemeIcon.asClassName(Codicon.arrowRight); + return $('div', {}, arrow); + } + + let marginDomNode: HTMLElement | undefined = undefined; + if (a.diff && a.diff.modifiedRange.isEmpty) { + marginDomNode = createViewZoneMarginArrow(); + } + modViewZones.push({ afterLineNumber: a.modifiedRange.endLineNumberExclusive - 1, domNode: createFakeLinesDiv(), heightInPx: -delta, + marginDomNode, }); } }