Skip to content

Commit 4dd7a52

Browse files
authored
Implements diff editor revert button. (microsoft#184909)
1 parent 02ec02e commit 4dd7a52

File tree

4 files changed

+99
-41
lines changed

4 files changed

+99
-41
lines changed

src/vs/editor/browser/widget/diffEditorWidget2/decorations.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { Codicon } from 'vs/base/common/codicons';
7+
import { MarkdownString } from 'vs/base/common/htmlContent';
78
import { ThemeIcon } from 'vs/base/common/themables';
89
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
910
import { localize } from 'vs/nls';
@@ -37,3 +38,10 @@ export const diffDeleteDecoration = ModelDecorationOptions.register({
3738
className: 'char-delete',
3839
description: 'char-delete',
3940
});
41+
42+
export const arrowRevertChange = ModelDecorationOptions.register({
43+
description: 'diff-editor-arrow-revert-change',
44+
glyphMarginHoverMessage: new MarkdownString(undefined, { isTrusted: true, supportThemeIcons: true }).appendMarkdown(localize('revertChangeHoverMessage', 'Click to revert change')),
45+
glyphMarginClassName: 'arrow-revert-change ' + ThemeIcon.asClassName(Codicon.arrowRight),
46+
zIndex: 10001,
47+
});

src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ import { isDefined } from 'vs/base/common/types';
1414
import { Constants } from 'vs/base/common/uint';
1515
import 'vs/css!./style';
1616
import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration';
17-
import { ICodeEditor, IDiffEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser';
17+
import { ICodeEditor, IDiffEditor, IDiffEditorConstructionOptions, IMouseTargetViewZone } from 'vs/editor/browser/editorBrowser';
1818
import { EditorExtensionsRegistry, IDiffEditorContributionDescription } from 'vs/editor/browser/editorExtensions';
1919
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
2020
import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget';
2121
import { IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEditorWidget';
22-
import { diffAddDecoration, diffDeleteDecoration, diffFullLineAddDecoration, diffFullLineDeleteDecoration } from 'vs/editor/browser/widget/diffEditorWidget2/decorations';
22+
import { arrowRevertChange, diffAddDecoration, diffDeleteDecoration, diffFullLineAddDecoration, diffFullLineDeleteDecoration } from 'vs/editor/browser/widget/diffEditorWidget2/decorations';
2323
import { DiffEditorSash } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorSash';
2424
import { ViewZoneManager } from 'vs/editor/browser/widget/diffEditorWidget2/lineAlignment';
2525
import { MovedBlocksLinesPart } from 'vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines';
@@ -40,6 +40,8 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
4040
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
4141
import { DelegatingEditor } from './delegatingEditorImpl';
4242
import { DiffMapping, DiffModel } from './diffModel';
43+
import { Range } from 'vs/editor/common/core/range';
44+
import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
4345

4446
const diffEditorDefaultOptions: ValidDiffEditorBaseOptions = {
4547
enableSplitViewResizing: true,
@@ -142,7 +144,16 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
142144
this._register(keepAlive(this._sash, true));
143145

144146
this._register(new UnchangedRangesFeature(this._originalEditor, this._modifiedEditor, this._diffModel));
145-
this._register(this._instantiationService.createInstance(ViewZoneManager, this._originalEditor, this._modifiedEditor, this._diffModel, this._options.map(o => o.renderSideBySide)));
147+
this._register(
148+
this._instantiationService.createInstance(
149+
ViewZoneManager,
150+
this._originalEditor,
151+
this._modifiedEditor,
152+
this._diffModel,
153+
this._options.map((o) => o.renderSideBySide),
154+
this
155+
)
156+
);
146157

147158
this._register(this._instantiationService.createInstance(OverviewRulerPart,
148159
this._originalEditor,
@@ -229,6 +240,10 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
229240
originalDecorations.push({ range: i.originalRange, options: diffDeleteDecoration });
230241
modifiedDecorations.push({ range: i.modifiedRange, options: diffAddDecoration });
231242
}
243+
244+
if (!m.lineRangeMapping.modifiedRange.isEmpty) {
245+
modifiedDecorations.push({ range: Range.fromPositions(new Position(m.lineRangeMapping.modifiedRange.startLineNumber, 1)), options: arrowRevertChange });
246+
}
232247
}
233248

234249
if (currentMove) {
@@ -311,24 +326,32 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
311326
m.syncedMovedTexts.set(movedText, undefined);
312327
}));
313328
// Revert change when an arrow is clicked.
314-
/*TODO
315329
this._register(editor.onMouseDown(event => {
316330
if (!event.event.rightButton && event.target.position && event.target.element?.className.includes('arrow-revert-change')) {
317331
const lineNumber = event.target.position.lineNumber;
318-
const viewZone = event.target as editorBrowser.IMouseTargetViewZone | undefined;
319-
const change = this._diffComputationResult?.changes.find(c =>
320-
// delete change
321-
viewZone?.detail.afterLineNumber === c.modifiedStartLineNumber ||
322-
// other changes
323-
(c.modifiedEndLineNumber > 0 && c.modifiedStartLineNumber === lineNumber));
324-
if (change) {
325-
this.revertChange(change);
332+
const viewZone = event.target as IMouseTargetViewZone | undefined;
333+
334+
const model = this._diffModel.get();
335+
if (!model) {
336+
return;
337+
}
338+
const diffs = model.diff.get()?.mappings;
339+
if (!diffs) {
340+
return;
341+
}
342+
const diff = diffs.find(d =>
343+
viewZone?.detail.afterLineNumber === d.lineRangeMapping.modifiedRange.startLineNumber - 1 ||
344+
d.lineRangeMapping.modifiedRange.startLineNumber === lineNumber
345+
);
346+
if (!diff) {
347+
return;
326348
}
349+
this.revert(diff.lineRangeMapping);
350+
327351
event.event.stopPropagation();
328-
this._updateDecorations();
329352
return;
330353
}
331-
}));*/
354+
}));
332355

333356
return editor;
334357
}
@@ -579,6 +602,17 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor {
579602
};
580603
}
581604

605+
public revert(diff: LineRangeMapping): void {
606+
const model = this._model.get();
607+
if (!model) {
608+
return;
609+
}
610+
const originalText = model.original.getValueInRange(diff.originalRange.toExclusiveRange());
611+
this._modifiedEditor.executeEdits('diffEditor', [
612+
{ range: diff.modifiedRange.toExclusiveRange(), text: originalText }
613+
]);
614+
}
615+
582616
private _goTo(diff: DiffMapping): void {
583617
this._modifiedEditor.setPosition(new Position(diff.lineRangeMapping.modifiedRange.startLineNumber, 1));
584618
this._modifiedEditor.revealRangeInCenter(diff.lineRangeMapping.modifiedRange.toExclusiveRange());

src/vs/editor/browser/widget/diffEditorWidget2/inlineDiffDeletedCodeMargin.ts

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { isIOS } from 'vs/base/common/platform';
1111
import { ThemeIcon } from 'vs/base/common/themables';
1212
import { IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser';
1313
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
14+
import { DiffEditorWidget2 } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2';
1415
import { EditorOption } from 'vs/editor/common/config/editorOptions';
1516
import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
1617
import { EndOfLineSequence, ITextModel } from 'vs/editor/common/model';
@@ -42,9 +43,10 @@ export class InlineDiffDeletedCodeMargin extends Disposable {
4243
constructor(
4344
private readonly _getViewZoneId: () => string,
4445
private readonly _marginDomNode: HTMLElement,
45-
private readonly editor: CodeEditorWidget,
46-
private readonly diff: LineRangeMapping,
47-
private readonly viewLineCounts: number[],
46+
private readonly _modifiedEditor: CodeEditorWidget,
47+
private readonly _diff: LineRangeMapping,
48+
private readonly _editor: DiffEditorWidget2,
49+
private readonly _viewLineCounts: number[],
4850
private readonly _originalTextModel: ITextModel,
4951
private readonly _contextMenuService: IContextMenuService,
5052
private readonly _clipboardService: IClipboardService,
@@ -57,46 +59,46 @@ export class InlineDiffDeletedCodeMargin extends Disposable {
5759
this._diffActions = document.createElement('div');
5860
this._diffActions.className = ThemeIcon.asClassName(Codicon.lightBulb) + ' lightbulb-glyph';
5961
this._diffActions.style.position = 'absolute';
60-
const lineHeight = this.editor.getOption(EditorOption.lineHeight);
62+
const lineHeight = this._modifiedEditor.getOption(EditorOption.lineHeight);
6163
this._diffActions.style.right = '0px';
6264
this._diffActions.style.visibility = 'hidden';
6365
this._diffActions.style.height = `${lineHeight}px`;
6466
this._diffActions.style.lineHeight = `${lineHeight}px`;
6567
this._marginDomNode.appendChild(this._diffActions);
6668

6769
const actions: Action[] = [];
68-
const isDeletion = diff.modifiedRange.isEmpty;
70+
const isDeletion = _diff.modifiedRange.isEmpty;
6971

7072
// default action
7173
actions.push(new Action(
7274
'diff.clipboard.copyDeletedContent',
7375
isDeletion
74-
? (diff.originalRange.length > 1
76+
? (_diff.originalRange.length > 1
7577
? localize('diff.clipboard.copyDeletedLinesContent.label', "Copy deleted lines")
7678
: localize('diff.clipboard.copyDeletedLinesContent.single.label', "Copy deleted line"))
77-
: (diff.originalRange.length > 1
79+
: (_diff.originalRange.length > 1
7880
? localize('diff.clipboard.copyChangedLinesContent.label', "Copy changed lines")
7981
: localize('diff.clipboard.copyChangedLinesContent.single.label', "Copy changed line")),
8082
undefined,
8183
true,
8284
async () => {
83-
const originalText = this._originalTextModel.getValueInRange(diff.originalRange.toExclusiveRange());
85+
const originalText = this._originalTextModel.getValueInRange(_diff.originalRange.toExclusiveRange());
8486
await this._clipboardService.writeText(originalText);
8587
}
8688
));
8789

8890
let currentLineNumberOffset = 0;
8991
let copyLineAction: Action | undefined = undefined;
90-
if (diff.originalRange.length > 1) {
92+
if (_diff.originalRange.length > 1) {
9193
copyLineAction = new Action(
9294
'diff.clipboard.copyDeletedLineContent',
9395
isDeletion
94-
? localize('diff.clipboard.copyDeletedLineContent.label', "Copy deleted line ({0})", diff.originalRange.startLineNumber)
95-
: localize('diff.clipboard.copyChangedLineContent.label', "Copy changed line ({0})", diff.originalRange.startLineNumber),
96+
? localize('diff.clipboard.copyDeletedLineContent.label', "Copy deleted line ({0})", _diff.originalRange.startLineNumber)
97+
: localize('diff.clipboard.copyChangedLineContent.label', "Copy changed line ({0})", _diff.originalRange.startLineNumber),
9698
undefined,
9799
true,
98100
async () => {
99-
let lineContent = this._originalTextModel.getLineContent(diff.originalRange.startLineNumber + currentLineNumberOffset);
101+
let lineContent = this._originalTextModel.getLineContent(_diff.originalRange.startLineNumber + currentLineNumberOffset);
100102
if (lineContent === '') {
101103
// empty line -> new line
102104
const eof = this._originalTextModel.getEndOfLineSequence();
@@ -109,21 +111,18 @@ export class InlineDiffDeletedCodeMargin extends Disposable {
109111
actions.push(copyLineAction);
110112
}
111113

112-
const readOnly = editor.getOption(EditorOption.readOnly);
114+
const readOnly = _modifiedEditor.getOption(EditorOption.readOnly);
113115
if (!readOnly) {
114116
actions.push(new Action('diff.inline.revertChange', localize('diff.inline.revertChange.label', "Revert this change"), undefined, true, async () => {
115-
const originalText = this._originalTextModel.getValueInRange(this.diff.originalRange.toExclusiveRange());
116-
editor.executeEdits('diffEditor', [
117-
{ range: this.diff.modifiedRange.toExclusiveRange(), text: originalText }
118-
]);
117+
this._editor.revert(this._diff);
119118
}));
120119
}
121120

122-
const useShadowDOM = editor.getOption(EditorOption.useShadowDOM) && !isIOS; // Do not use shadow dom on IOS #122035
121+
const useShadowDOM = _modifiedEditor.getOption(EditorOption.useShadowDOM) && !isIOS; // Do not use shadow dom on IOS #122035
123122

124123
const showContextMenu = (x: number, y: number) => {
125124
this._contextMenuService.showContextMenu({
126-
domForShadowRoot: useShadowDOM ? editor.getDomNode() ?? undefined : undefined,
125+
domForShadowRoot: useShadowDOM ? _modifiedEditor.getDomNode() ?? undefined : undefined,
127126
getAnchor: () => {
128127
return {
129128
x,
@@ -134,8 +133,8 @@ export class InlineDiffDeletedCodeMargin extends Disposable {
134133
if (copyLineAction) {
135134
copyLineAction.label =
136135
isDeletion
137-
? localize('diff.clipboard.copyDeletedLineContent.label', "Copy deleted line ({0})", diff.originalRange.startLineNumber + currentLineNumberOffset)
138-
: localize('diff.clipboard.copyChangedLineContent.label', "Copy changed line ({0})", diff.originalRange.startLineNumber + currentLineNumberOffset);
136+
? localize('diff.clipboard.copyDeletedLineContent.label', "Copy deleted line ({0})", _diff.originalRange.startLineNumber + currentLineNumberOffset)
137+
: localize('diff.clipboard.copyChangedLineContent.label', "Copy changed line ({0})", _diff.originalRange.startLineNumber + currentLineNumberOffset);
139138
}
140139
return actions;
141140
},
@@ -150,7 +149,7 @@ export class InlineDiffDeletedCodeMargin extends Disposable {
150149
showContextMenu(e.posx, top + height + pad);
151150
}));
152151

153-
this._register(editor.onMouseMove((e: IEditorMouseEvent) => {
152+
this._register(_modifiedEditor.onMouseMove((e: IEditorMouseEvent) => {
154153
if ((e.target.type === MouseTargetType.CONTENT_VIEW_ZONE || e.target.type === MouseTargetType.GUTTER_VIEW_ZONE) && e.target.detail.viewZoneId === this._getViewZoneId()) {
155154
currentLineNumberOffset = this._updateLightBulbPosition(this._marginDomNode, e.event.browserEvent.y, lineHeight);
156155
this.visibility = true;
@@ -159,7 +158,7 @@ export class InlineDiffDeletedCodeMargin extends Disposable {
159158
}
160159
}));
161160

162-
this._register(editor.onMouseDown((e: IEditorMouseEvent) => {
161+
this._register(_modifiedEditor.onMouseDown((e: IEditorMouseEvent) => {
163162
if (!e.event.rightButton) {
164163
return;
165164
}
@@ -182,10 +181,10 @@ export class InlineDiffDeletedCodeMargin extends Disposable {
182181
const lineNumberOffset = Math.floor(offset / lineHeight);
183182
const newTop = lineNumberOffset * lineHeight;
184183
this._diffActions.style.top = `${newTop}px`;
185-
if (this.viewLineCounts) {
184+
if (this._viewLineCounts) {
186185
let acc = 0;
187-
for (let i = 0; i < this.viewLineCounts.length; i++) {
188-
acc += this.viewLineCounts[i];
186+
for (let i = 0; i < this._viewLineCounts.length; i++) {
187+
acc += this._viewLineCounts[i];
189188
if (lineNumberOffset < acc) {
190189
return i;
191190
}

src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { $ } from 'vs/base/browser/dom';
67
import { ArrayQueue } from 'vs/base/common/arrays';
8+
import { Codicon } from 'vs/base/common/codicons';
79
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
810
import { IObservable, derived, observableFromEvent, observableSignalFromEvent, observableValue } from 'vs/base/common/observable';
911
import { autorun, autorunWithStore2 } from 'vs/base/common/observableImpl/autorun';
@@ -14,6 +16,7 @@ import { IViewZone } from 'vs/editor/browser/editorBrowser';
1416
import { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll';
1517
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
1618
import { diffDeleteDecoration, diffRemoveIcon } from 'vs/editor/browser/widget/diffEditorWidget2/decorations';
19+
import { DiffEditorWidget2 } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2';
1720
import { DiffMapping, DiffModel } from 'vs/editor/browser/widget/diffEditorWidget2/diffModel';
1821
import { InlineDiffDeletedCodeMargin } from 'vs/editor/browser/widget/diffEditorWidget2/inlineDiffDeletedCodeMargin';
1922
import { LineSource, RenderOptions, renderLines } from 'vs/editor/browser/widget/diffEditorWidget2/renderLines';
@@ -49,6 +52,7 @@ export class ViewZoneManager extends Disposable {
4952
private readonly _modifiedEditor: CodeEditorWidget,
5053
private readonly _diffModel: IObservable<DiffModel | undefined>,
5154
private readonly _renderSideBySide: IObservable<boolean>,
55+
private readonly _diffEditorWidget: DiffEditorWidget2,
5256
@IClipboardService private readonly _clipboardService: IClipboardService,
5357
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
5458
) {
@@ -190,10 +194,11 @@ export class ViewZoneManager extends Disposable {
190194
marginDomNode,
191195
this._modifiedEditor,
192196
a.diff,
197+
this._diffEditorWidget,
193198
result.viewLineCounts,
194199
this._originalEditor.getModel()!,
195200
this._contextMenuService,
196-
this._clipboardService
201+
this._clipboardService,
197202
)
198203
);
199204

@@ -245,10 +250,22 @@ export class ViewZoneManager extends Disposable {
245250
continue;
246251
}
247252

253+
function createViewZoneMarginArrow(): HTMLElement {
254+
const arrow = document.createElement('div');
255+
arrow.className = 'arrow-revert-change ' + ThemeIcon.asClassName(Codicon.arrowRight);
256+
return $('div', {}, arrow);
257+
}
258+
259+
let marginDomNode: HTMLElement | undefined = undefined;
260+
if (a.diff && a.diff.modifiedRange.isEmpty) {
261+
marginDomNode = createViewZoneMarginArrow();
262+
}
263+
248264
modViewZones.push({
249265
afterLineNumber: a.modifiedRange.endLineNumberExclusive - 1,
250266
domNode: createFakeLinesDiv(),
251267
heightInPx: -delta,
268+
marginDomNode,
252269
});
253270
}
254271
}

0 commit comments

Comments
 (0)