Skip to content

Commit 6579249

Browse files
authored
Improve side-by-side view (microsoft#238131)
improved side by side view
1 parent 8159b3e commit 6579249

File tree

4 files changed

+126
-79
lines changed

4 files changed

+126
-79
lines changed

src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/deletionView.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,13 @@ export class InlineEditsDeletionView extends Disposable {
138138
const rectangleOverlay = createRectangle(
139139
{
140140
topLeft: layoutInfo.codeStart1,
141-
topRight: layoutInfo.code1.deltaX(1),
142-
bottomLeft: layoutInfo.codeStart2.deltaY(1),
143-
bottomRight: layoutInfo.code2.deltaX(1).deltaY(1),
141+
width: layoutInfo.code1.x - layoutInfo.codeStart1.x + 1,
142+
height: layoutInfo.code2.y - layoutInfo.code1.y + 1,
144143
},
145144
layoutInfo.padding,
146145
layoutInfo.borderRadius,
147146
{ hideLeft: layoutInfo.horizontalScrollOffset !== 0 }
148-
);
147+
).build();
149148

150149
return [
151150
n.svgElem('path', {

src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/sideBySideDiff.ts

Lines changed: 86 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
* Copyright (c) Microsoft Corporation. All rights reserved.
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
5-
import { getWindow } from '../../../../../../base/browser/dom.js';
5+
import { $, getWindow } from '../../../../../../base/browser/dom.js';
66
import { ActionViewItem } from '../../../../../../base/browser/ui/actionbar/actionViewItems.js';
77
import { IAction } from '../../../../../../base/common/actions.js';
88
import { Color } from '../../../../../../base/common/color.js';
99
import { structuralEquals } from '../../../../../../base/common/equals.js';
1010
import { Disposable } from '../../../../../../base/common/lifecycle.js';
11-
import { IObservable, autorun, constObservable, derived, derivedObservableWithCache, derivedOpts, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js';
11+
import { IObservable, autorun, constObservable, derived, derivedObservableWithCache, derivedOpts, observableFromEvent } from '../../../../../../base/common/observable.js';
1212
import { MenuId, MenuItemAction } from '../../../../../../platform/actions/common/actions.js';
1313
import { ICommandService } from '../../../../../../platform/commands/common/commands.js';
1414
import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js';
@@ -30,7 +30,7 @@ import { ITextModel } from '../../../../../common/model.js';
3030
import { StickyScrollController } from '../../../../stickyScroll/browser/stickyScrollController.js';
3131
import { InlineCompletionContextKeys } from '../../controller/inlineCompletionContextKeys.js';
3232
import { CustomizedMenuWorkbenchToolBar } from '../../hintsWidget/inlineCompletionsHintsWidget.js';
33-
import { PathBuilder, StatusBarViewItem, getOffsetForPos, mapOutFalsy, maxContentWidthInRange, n } from './utils.js';
33+
import { PathBuilder, StatusBarViewItem, createRectangle, getOffsetForPos, mapOutFalsy, maxContentWidthInRange, n } from './utils.js';
3434
import { InlineEditWithChanges } from './viewAndDiffProducer.js';
3535
import { localize } from '../../../../../../nls.js';
3636

@@ -159,13 +159,13 @@ export class InlineEditsSideBySideDiff extends Disposable {
159159
return;
160160
}
161161

162-
const topEdit = layoutInfo.edit1;
163-
const bottomEdit = layoutInfo.edit2;
162+
const editorTopLeft = layoutInfo.editStart1.deltaY(layoutInfo.padding);
163+
const editorBottomLeft = layoutInfo.editStart2.deltaY(-layoutInfo.padding);
164164

165-
this.previewEditor.layout({ height: bottomEdit.y - topEdit.y, width: layoutInfo.previewEditorWidth });
166-
this.previewEditor.updateOptions({ padding: { top: layoutInfo.padding, bottom: layoutInfo.padding } });
167-
this._editorContainer.element.style.top = `${topEdit.y}px`;
168-
this._editorContainer.element.style.left = `${topEdit.x}px`;
165+
this.previewEditor.layout({ height: editorBottomLeft.y - editorTopLeft.y, width: layoutInfo.previewEditorWidth + 15 /* Make sure editor does not scroll horizontally */ });
166+
this._editorContainer.element.style.top = `${editorTopLeft.y}px`;
167+
this._editorContainer.element.style.left = `${editorTopLeft.x}px`;
168+
this._editorContainer.element.style.width = `${layoutInfo.previewEditorWidth}px`; // Set width to clip view zone
169169
}));
170170

171171
/*const toolbarDropdownVisible = observableFromEvent(this, this._toolbar.onDidChangeDropdownVisibility, (e) => e ?? false);
@@ -182,20 +182,16 @@ export class InlineEditsSideBySideDiff extends Disposable {
182182

183183
this._previewEditorObs.editor.setScrollLeft(layoutInfo.desiredPreviewEditorScrollLeft);
184184
}));
185-
186-
this._editorContainerTopLeft.set(this._previewEditorLayoutInfo.map(i => i?.edit1), undefined);
187185
}
188186

189187
private readonly _display = derived(this, reader => !!this._uiState.read(reader) ? 'block' : 'none');
190188

191189
private readonly previewRef = n.ref<HTMLDivElement>();
192190
private readonly toolbarRef = n.ref<HTMLDivElement>();
193191

194-
private readonly _editorContainerTopLeft = observableValue<IObservable<Point | undefined> | undefined>(this, undefined);
195-
196192
private readonly _editorContainer = n.div({
197193
class: ['editorContainer', this._editorObs.getOption(EditorOption.inlineSuggest).map(v => !v.edits.experimental.useGutterIndicator && 'showHover')],
198-
style: { position: 'absolute' },
194+
style: { position: 'absolute', overflow: 'hidden' },
199195
}, [
200196
n.div({ class: 'preview', style: {}, ref: this.previewRef }),
201197
n.div({ class: 'toolbar', style: {}, ref: this.toolbarRef }),
@@ -299,6 +295,7 @@ export class InlineEditsSideBySideDiff extends Disposable {
299295

300296
private readonly _previewEditorObs = observableCodeEditor(this.previewEditor);
301297

298+
private _activeViewZones: string[] = [];
302299
private readonly _updatePreviewEditor = derived(reader => {
303300
this._editorContainer.readEffect(reader);
304301

@@ -331,14 +328,32 @@ export class InlineEditsSideBySideDiff extends Disposable {
331328

332329
this.previewEditor.setHiddenAreas(hiddenAreas, undefined, true);
333330

331+
// TODO: is this the proper way to handle viewzones?
332+
const previousViewZones = [...this._activeViewZones];
333+
this._activeViewZones = [];
334+
335+
const reducedLinesCount = (range.endLineNumberExclusive - range.startLineNumber) - uiState.newTextLineCount;
336+
this.previewEditor.changeViewZones((changeAccessor) => {
337+
previousViewZones.forEach(id => changeAccessor.removeZone(id));
338+
339+
if (reducedLinesCount > 0) {
340+
this._activeViewZones.push(changeAccessor.addZone({
341+
afterLineNumber: range.startLineNumber + uiState.newTextLineCount - 1,
342+
heightInLines: reducedLinesCount,
343+
showInHiddenAreas: true,
344+
domNode: $('div.diagonal-fill.inline-edits-view-zone'),
345+
}));
346+
}
347+
});
348+
334349
}).recomputeInitiallyAndOnChange(this._store);
335350

336351
private readonly _previewEditorWidth = derived(this, reader => {
337352
const edit = this._edit.read(reader);
338353
if (!edit) { return 0; }
339354
this._updatePreviewEditor.read(reader);
340355

341-
return maxContentWidthInRange(this._previewEditorObs, edit.modifiedLineRange, reader) + 10;
356+
return maxContentWidthInRange(this._previewEditorObs, edit.modifiedLineRange, reader);
342357
});
343358

344359
private readonly _cursorPosIfTouchesEdit = derived(this, reader => {
@@ -409,7 +424,7 @@ export class InlineEditsSideBySideDiff extends Disposable {
409424
const cursorPos = this._cursorPosIfTouchesEdit.read(reader);
410425

411426
const maxPreviewEditorLeft = Math.max(
412-
// We're starting from the content area right and moving it left by IN_EDITOR_DISPLACEMENT and also by an ammount to ensure some mimum desired width
427+
// We're starting from the content area right and moving it left by IN_EDITOR_DISPLACEMENT and also by an amount to ensure some minimum desired width
413428
editorContentAreaWidth + horizontalScrollOffset - IN_EDITOR_DISPLACEMENT - Math.max(0, desiredMinimumWidth - maximumAvailableWidth),
414429
// But we don't want that the moving left ends up covering the cursor, so this will push it to the right again
415430
Math.min(
@@ -438,36 +453,62 @@ export class InlineEditsSideBySideDiff extends Disposable {
438453

439454
const codeLeft = editorLayout.contentLeft;
440455

441-
const code1 = new Point(left, selectionTop);
442-
const codeStart1 = new Point(codeLeft, selectionTop);
443-
const code2 = new Point(left, selectionBottom);
444-
const codeStart2 = new Point(codeLeft, selectionBottom);
456+
let code1 = new Point(left, selectionTop);
457+
let codeStart1 = new Point(codeLeft, selectionTop);
458+
let code2 = new Point(left, selectionBottom);
459+
let codeStart2 = new Point(codeLeft, selectionBottom);
460+
461+
const editHeight = this._editor.getOption(EditorOption.lineHeight) * inlineEdit.modifiedLineRange.length;
445462
const codeHeight = selectionBottom - selectionTop;
463+
const previewEditorHeight = Math.max(codeHeight, editHeight);
446464

447-
const codeEditDistRange = inlineEdit.modifiedLineRange.length === inlineEdit.originalLineRange.length
465+
const editIsSameHeight = codeHeight === previewEditorHeight;
466+
const codeEditDistRange = editIsSameHeight
448467
? new OffsetRange(4, 61)
449468
: new OffsetRange(60, 61);
450469

451470
const clipped = dist === 0;
471+
const PADDING = 4;
452472

453-
const codeEditDist = codeEditDistRange.clip(dist);
454-
const editHeight = this._editor.getOption(EditorOption.lineHeight) * inlineEdit.modifiedLineRange.length;
473+
const codeEditDist = editIsSameHeight ? PADDING : codeEditDistRange.clip(dist); // TODO: Is there a better way to specify the distance?
455474

456475
const previewEditorWidth = Math.min(previewContentWidth, remainingWidthRightOfEditor + editorLayout.width - editorLayout.contentLeft - codeEditDist);
457476

458-
const PADDING = 4;
459-
460-
const edit1 = new Point(left + codeEditDist, selectionTop);
461-
const edit2 = new Point(left + codeEditDist, selectionTop + editHeight + PADDING * 2);
477+
let editStart1 = new Point(left + codeEditDist, selectionTop);
478+
let edit1 = editStart1.deltaX(previewEditorWidth);
479+
let editStart2 = new Point(left + codeEditDist, selectionTop + previewEditorHeight);
480+
let edit2 = editStart2.deltaX(previewEditorWidth);
481+
482+
// padding
483+
const isInsertion = codeHeight === 0;
484+
if (!isInsertion) {
485+
codeStart1 = codeStart1.deltaY(-PADDING).deltaX(-PADDING);
486+
code1 = code1.deltaY(-PADDING);
487+
codeStart2 = codeStart2.deltaY(PADDING).deltaX(-PADDING);
488+
code2 = code2.deltaY(PADDING);
489+
490+
editStart1 = editStart1.deltaY(-PADDING);
491+
edit1 = edit1.deltaY(-PADDING).deltaX(PADDING);
492+
editStart2 = editStart2.deltaY(PADDING);
493+
edit2 = edit2.deltaY(PADDING).deltaX(PADDING);
494+
} else {
495+
// Align top of edit with insertion line
496+
edit1 = edit1.deltaX(PADDING);
497+
editStart2 = editStart2.deltaY(2 * PADDING);
498+
edit2 = edit2.deltaY(2 * PADDING).deltaX(PADDING);
499+
}
462500

463501
return {
464502
code1,
465503
codeStart1,
466504
code2,
467505
codeStart2,
468506
codeHeight,
507+
codeScrollLeft: horizontalScrollOffset,
469508

509+
editStart1,
470510
edit1,
511+
editStart2,
471512
edit2,
472513
editHeight,
473514
maxContentWidth,
@@ -503,31 +544,19 @@ export class InlineEditsSideBySideDiff extends Disposable {
503544
private readonly _extendedModifiedPath = derived(reader => {
504545
const layoutInfo = this._previewEditorLayoutInfo.read(reader);
505546
if (!layoutInfo) { return undefined; }
506-
const width = layoutInfo.previewEditorWidth + layoutInfo.padding;
507-
508-
const topLeft = layoutInfo.edit1;
509-
const topRight = layoutInfo.edit1.deltaX(width);
510-
const topRightBefore = topRight.deltaX(-layoutInfo.borderRadius);
511-
const topRightAfter = topRight.deltaY(layoutInfo.borderRadius);
512-
513-
const bottomLeft = layoutInfo.edit2;
514-
const bottomRight = bottomLeft.deltaX(width);
515-
const bottomRightBefore = bottomRight.deltaY(-layoutInfo.borderRadius);
516-
const bottomRightAfter = bottomRight.deltaX(-layoutInfo.borderRadius);
517-
518-
const extendedModifiedPathBuilder = new PathBuilder()
519-
.moveTo(layoutInfo.code1)
520-
.lineTo(topLeft)
521-
.lineTo(topRightBefore)
522-
.curveTo(topRight, topRightAfter)
523-
.lineTo(bottomRightBefore)
524-
.curveTo(bottomRight, bottomRightAfter)
525-
.lineTo(bottomLeft);
526-
527-
if (layoutInfo.edit2.y !== layoutInfo.code2.y) {
528-
extendedModifiedPathBuilder.curveTo2(layoutInfo.edit2.deltaX(-20), layoutInfo.code2.deltaX(20), layoutInfo.code2.deltaX(0));
547+
548+
const extendedModifiedPathBuilder = createRectangle(
549+
{ topLeft: layoutInfo.editStart1, width: layoutInfo.edit1.x - layoutInfo.editStart1.x, height: layoutInfo.editStart2.y - layoutInfo.editStart1.y },
550+
0,
551+
{ topLeft: 0, bottomLeft: 0, topRight: layoutInfo.borderRadius, bottomRight: layoutInfo.borderRadius },
552+
{ hideLeft: true }
553+
);
554+
555+
if (layoutInfo.editStart2.y !== layoutInfo.code2.y) {
556+
extendedModifiedPathBuilder.moveTo(layoutInfo.editStart2);
557+
extendedModifiedPathBuilder.curveTo2(layoutInfo.editStart2.deltaX(-20), layoutInfo.code2.deltaX(20), layoutInfo.code2.deltaX(0));
529558
}
530-
extendedModifiedPathBuilder.lineTo(layoutInfo.code2);
559+
extendedModifiedPathBuilder.lineTo(layoutInfo.code2).moveTo(layoutInfo.code1).lineTo(layoutInfo.editStart1);
531560
return extendedModifiedPathBuilder.build();
532561
});
533562

@@ -601,13 +630,12 @@ export class InlineEditsSideBySideDiff extends Disposable {
601630
return [
602631
n.svgElem('path', {
603632
class: 'originalOverlay',
604-
d: layoutInfoObs.map(layoutInfo => new PathBuilder()
605-
.moveTo(layoutInfo.code2)
606-
.lineTo(layoutInfo.codeStart2)
607-
.lineTo(layoutInfo.codeStart1)
608-
.lineTo(layoutInfo.code1)
609-
.build()
610-
),
633+
d: layoutInfoObs.map(layoutInfo => createRectangle(
634+
{ topLeft: layoutInfo.codeStart1, width: layoutInfo.code1.x - layoutInfo.codeStart1.x, height: layoutInfo.code2.y - layoutInfo.code1.y },
635+
0,
636+
{ topLeft: layoutInfo.borderRadius, bottomLeft: layoutInfo.borderRadius, topRight: 0, bottomRight: 0 },
637+
{ hideRight: true, hideLeft: layoutInfo.codeScrollLeft !== 0 }
638+
).build()),
611639
style: {
612640
fill: 'var(--vscode-inlineEdit-originalBackground, transparent)',
613641
stroke: 'var(--vscode-inlineEdit-originalBorder)',

src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils.ts

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -163,32 +163,48 @@ export class PathBuilder {
163163
}
164164
}
165165

166+
// Arguments are a bit messy currently, could be improved
166167
export function createRectangle(
167-
corners: { topLeft: Point; topRight: Point; bottomLeft: Point; bottomRight: Point },
168+
layout: { topLeft: Point; width: number; height: number },
168169
padding: number | { top: number; right: number; bottom: number; left: number },
169-
borderRadius: number,
170+
borderRadius: number | { topLeft: number; topRight: number; bottomLeft: number; bottomRight: number },
170171
options: { hideLeft?: boolean; hideRight?: boolean; hideTop?: boolean; hideBottom?: boolean } = {}
171-
): string {
172+
): PathBuilder {
173+
174+
const topLeftInner = layout.topLeft;
175+
const topRightInner = topLeftInner.deltaX(layout.width);
176+
const bottomLeftInner = topLeftInner.deltaY(layout.height);
177+
const bottomRightInner = bottomLeftInner.deltaX(layout.width);
178+
179+
// padding
172180
const { top: paddingTop, bottom: paddingBottom, left: paddingLeft, right: paddingRight } = typeof padding === 'number' ?
173181
{ top: padding, bottom: padding, left: padding, right: padding }
174182
: padding;
175183

184+
// corner radius
185+
const { topLeft: radiusTL, topRight: radiusTR, bottomLeft: radiusBL, bottomRight: radiusBR } = typeof borderRadius === 'number' ?
186+
{ topLeft: borderRadius, topRight: borderRadius, bottomLeft: borderRadius, bottomRight: borderRadius } :
187+
borderRadius;
188+
189+
const totalHeight = layout.height + paddingTop + paddingBottom;
190+
const totalWidth = layout.width + paddingLeft + paddingRight;
191+
176192
// The path is drawn from bottom left at the end of the rounded corner in a clockwise direction
177193
// Before: before the rounded corner
178194
// After: after the rounded corner
179-
const topLeft = corners.topLeft.deltaX(-paddingLeft).deltaY(-paddingTop);
180-
const topRight = corners.topRight.deltaX(paddingRight).deltaY(-paddingTop);
181-
const topLeftBefore = topLeft.deltaY(borderRadius);
182-
const topLeftAfter = topLeft.deltaX(borderRadius);
183-
const topRightBefore = topRight.deltaX(-borderRadius);
184-
const topRightAfter = topRight.deltaY(borderRadius);
185-
186-
const bottomLeft = corners.bottomLeft.deltaX(-paddingLeft).deltaY(paddingBottom);
187-
const bottomRight = corners.bottomRight.deltaX(paddingRight).deltaY(paddingBottom);
188-
const bottomLeftBefore = bottomLeft.deltaX(borderRadius);
189-
const bottomLeftAfter = bottomLeft.deltaY(-borderRadius);
190-
const bottomRightBefore = bottomRight.deltaY(-borderRadius);
191-
const bottomRightAfter = bottomRight.deltaX(-borderRadius);
195+
const topLeft = topLeftInner.deltaX(-paddingLeft).deltaY(-paddingTop);
196+
const topRight = topRightInner.deltaX(paddingRight).deltaY(-paddingTop);
197+
const topLeftBefore = topLeft.deltaY(Math.min(radiusTL, totalHeight / 2));
198+
const topLeftAfter = topLeft.deltaX(Math.min(radiusTL, totalWidth / 2));
199+
const topRightBefore = topRight.deltaX(-Math.min(radiusTR, totalWidth / 2));
200+
const topRightAfter = topRight.deltaY(Math.min(radiusTR, totalHeight / 2));
201+
202+
const bottomLeft = bottomLeftInner.deltaX(-paddingLeft).deltaY(paddingBottom);
203+
const bottomRight = bottomRightInner.deltaX(paddingRight).deltaY(paddingBottom);
204+
const bottomLeftBefore = bottomLeft.deltaX(Math.min(radiusBL, totalWidth / 2));
205+
const bottomLeftAfter = bottomLeft.deltaY(-Math.min(radiusBL, totalHeight / 2));
206+
const bottomRightBefore = bottomRight.deltaY(-Math.min(radiusBR, totalHeight / 2));
207+
const bottomRightAfter = bottomRight.deltaX(-Math.min(radiusBR, totalWidth / 2));
192208

193209
const path = new PathBuilder();
194210

@@ -224,7 +240,7 @@ export function createRectangle(
224240
path.curveTo(bottomLeft, bottomLeftAfter);
225241
}
226242

227-
return path.build();
243+
return path;
228244
}
229245

230246
type Value<T> = T | IObservable<T>;

src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@
133133
}
134134
}
135135
}
136+
137+
.inline-edits-view-zone.diagonal-fill {
138+
opacity: 0.5;
139+
}
136140
}
137141
}
138142

0 commit comments

Comments
 (0)