From 243da33637ab53974e5c88e73b93e039d3b00a24 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 29 Jan 2025 16:37:42 +0100 Subject: [PATCH] Fix misalignment of arrow and suggestion box in NES Insertion View (#239104) fix https://github.com/microsoft/vscode-copilot/issues/12461 --- src/vs/editor/browser/rect.ts | 8 +++ .../view/inlineEdits/gutterIndicatorView.ts | 6 ++- .../browser/view/inlineEdits/insertionView.ts | 54 ++++++++++++------- .../browser/view/inlineEdits/view.ts | 25 +++++++-- 4 files changed, 67 insertions(+), 26 deletions(-) diff --git a/src/vs/editor/browser/rect.ts b/src/vs/editor/browser/rect.ts index 09a93216f26..308e0395c91 100644 --- a/src/vs/editor/browser/rect.ts +++ b/src/vs/editor/browser/rect.ts @@ -138,7 +138,15 @@ export class Rect { return new Rect(this.left - delta, this.top, this.right - delta, this.bottom); } + moveRight(delta: number): Rect { + return new Rect(this.left + delta, this.top, this.right + delta, this.bottom); + } + moveUp(delta: number): Rect { return new Rect(this.left, this.top - delta, this.right, this.bottom - delta); } + + moveDown(delta: number): Rect { + return new Rect(this.left, this.top + delta, this.right, this.bottom + delta); + } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts index 5091bc0cc27..ec949f05a51 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/gutterIndicatorView.ts @@ -73,6 +73,7 @@ export class InlineEditsGutterIndicator extends Disposable { constructor( private readonly _editorObs: ObservableCodeEditor, private readonly _originalRange: IObservable, + private readonly _verticalOffset: IObservable, private readonly _model: IObservable, private readonly _isHoveringOverInlineEdit: IObservable, private readonly _focusIsInMenu: ISettableObservable, @@ -130,7 +131,8 @@ export class InlineEditsGutterIndicator extends Disposable { const lineHeight = this._editorObs.getOption(EditorOption.lineHeight).read(reader); - const pillRect = targetRect.withHeight(lineHeight).withWidth(22); + const pillOffset = this._verticalOffset.read(reader); + const pillRect = targetRect.withHeight(lineHeight).withWidth(22).moveDown(pillOffset); const pillRectMoved = pillRect.moveToBeContainedIn(viewPortWithStickyScroll); const rect = targetRect; @@ -142,7 +144,7 @@ export class InlineEditsGutterIndicator extends Disposable { return { rect, iconRect, - arrowDirection: (iconRect.top === targetRect.top ? 'right' as const + arrowDirection: (targetRect.containsRect(iconRect) ? 'right' as const : iconRect.top > targetRect.top ? 'top' as const : 'bottom' as const), docked: rect.containsRect(iconRect) && viewPortWithStickyScroll.containsRect(iconRect), }; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts index 1361c4b7c55..73539134b37 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/insertionView.ts @@ -11,6 +11,7 @@ import { observableCodeEditor } from '../../../../../browser/observableCodeEdito import { Point } from '../../../../../browser/point.js'; import { LineSource, renderLines, RenderOptions } from '../../../../../browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; import { EditorOption } from '../../../../../common/config/editorOptions.js'; +import { LineRange } from '../../../../../common/core/lineRange.js'; import { Position } from '../../../../../common/core/position.js'; import { Range } from '../../../../../common/core/range.js'; import { ILanguageService } from '../../../../../common/languages/language.js'; @@ -112,6 +113,32 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits return Math.max(...lineWidths); }); + private readonly _trimVertically = derived(this, reader => { + const text = this._state.read(reader)?.text; + if (!text || text.trim() === '') { + return { top: 0, bottom: 0 }; + } + + // Adjust for leading/trailing newlines + const lineHeight = this._editor.getOption(EditorOption.lineHeight); + let topTrim = 0; + let bottomTrim = 0; + + let i = 0; + for (; i < text.length && text[i] === '\n'; i++) { + topTrim += lineHeight; + } + + for (let j = text.length - 1; j > i && text[j] === '\n'; j--) { + bottomTrim += lineHeight; + } + + return { top: topTrim, bottom: bottomTrim }; + }); + + public readonly startLineOffset = this._trimVertically.map(v => v.top); + public readonly originalLines = this._state.map(s => s ? new LineRange(s.lineNumber, s.lineNumber + 2) : undefined); + private readonly _overlayLayout = derivedWithStore(this, (reader, store) => { this._ghostText.read(reader); const state = this._state.read(reader); @@ -122,35 +149,22 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits // Update the overlay when the position changes this._editorObs.observePosition(observableValue(this, new Position(state.lineNumber, state.column)), store).read(reader); - const lineHeight = this._editor.getOption(EditorOption.lineHeight); - const scrollTop = this._editorObs.scrollTop.read(reader); const editorLayout = this._editorObs.layoutInfo.read(reader); const horizontalScrollOffset = this._editorObs.scrollLeft.read(reader); const left = editorLayout.contentLeft + this._editorMaxContentWidthInRange.read(reader) - horizontalScrollOffset; - - let height = this._ghostTextView.height.read(reader); - let top = this._editor.getTopForLineNumber(state.lineNumber) - scrollTop; - - if (state.text.trim() !== '') { - // Adjust for leading/trailing newlines - let i = 0; - for (; i < state.text.length && state.text[i] === '\n'; i++) { - height -= lineHeight; - top += lineHeight; - } - for (let j = state.text.length - 1; j > i && state.text[j] === '\n'; j--) { - height -= lineHeight; - } - } - const codeLeft = editorLayout.contentLeft; - const bottom = top + height; - if (left <= codeLeft) { return null; } + const { top: topTrim, bottom: bottomTrim } = this._trimVertically.read(reader); + + const scrollTop = this._editorObs.scrollTop.read(reader); + const height = this._ghostTextView.height.read(reader) - topTrim - bottomTrim; + const top = this._editor.getTopForLineNumber(state.lineNumber) - scrollTop + topTrim; + const bottom = top + height; + const code1 = new Point(left, top); const codeStart1 = new Point(codeLeft, top); const code2 = new Point(left, bottom); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts index 6f980b72df5..4bc8b06bc73 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.ts @@ -185,12 +185,29 @@ export class InlineEditsView extends Disposable { || this._lineReplacementView.read(reader).some(v => v.isHovered.read(reader)); }); + private readonly _gutterIndicatorOffset = derived(this, reader => { + // TODO: have a better way to tell the gutter indicator view where the edit is inside a viewzone + if (this._uiState.read(reader)?.state?.kind === 'insertionMultiLine') { + return this._insertion.startLineOffset.read(reader); + } + return 0; + }); + + private readonly _originalDisplayRange = derived(this, reader => { + const state = this._uiState.read(reader); + if (state?.state?.kind === 'insertionMultiLine') { + return this._insertion.originalLines.read(reader); + } + return state?.originalDisplayRange; + }); + protected readonly _indicator = this._register(autorunWithStore((reader, store) => { if (this._useGutterIndicator.read(reader)) { store.add(this._instantiationService.createInstance( InlineEditsGutterIndicator, this._editorObs, - this._uiState.map(s => s && s.originalDisplayRange), + this._originalDisplayRange, + this._gutterIndicatorOffset, this._model, this._inlineEditsIsHovered, this._focusIsInMenu, @@ -200,9 +217,9 @@ export class InlineEditsView extends Disposable { this._editorObs, derived(reader => { const state = this._uiState.read(reader); - if (!state || !state.state) { return undefined; } - const range = state.originalDisplayRange; - const top = this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader); + const range = this._originalDisplayRange.read(reader); + if (!state || !state.state || !range) { return undefined; } + const top = this._editor.getTopForLineNumber(range.startLineNumber) - this._editorObs.scrollTop.read(reader) + this._gutterIndicatorOffset.read(reader); return { editTop: top, showAlways: state.state.kind !== 'sideBySide' }; }), this._model,