Fix misalignment of arrow and suggestion box in NES Insertion View (#239104)

fix https://github.com/microsoft/vscode-copilot/issues/12461
This commit is contained in:
Benjamin Christopher Simmonds
2025-01-29 16:37:42 +01:00
committed by GitHub
parent fa2f90b630
commit 243da33637
4 changed files with 67 additions and 26 deletions
+8
View File
@@ -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);
}
}
@@ -73,6 +73,7 @@ export class InlineEditsGutterIndicator extends Disposable {
constructor(
private readonly _editorObs: ObservableCodeEditor,
private readonly _originalRange: IObservable<LineRange | undefined>,
private readonly _verticalOffset: IObservable<number>,
private readonly _model: IObservable<InlineCompletionsModel | undefined>,
private readonly _isHoveringOverInlineEdit: IObservable<boolean>,
private readonly _focusIsInMenu: ISettableObservable<boolean>,
@@ -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),
};
@@ -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);
@@ -185,12 +185,29 @@ export class InlineEditsView extends Disposable {
|| this._lineReplacementView.read(reader).some(v => v.isHovered.read(reader));
});
private readonly _gutterIndicatorOffset = derived<number>(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<IInlineEditsIndicatorState | undefined>(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,