Merge pull request #282537 from microsoft/benibenj/sacred-herring

Fix freeze issue in ghost text rendering
This commit is contained in:
Benjamin Christopher Simmonds
2025-12-10 18:42:49 +01:00
committed by GitHub
2 changed files with 47 additions and 23 deletions
@@ -28,7 +28,7 @@ export class LineDecoration {
);
}
public static equalsArr(a: LineDecoration[], b: LineDecoration[]): boolean {
public static equalsArr(a: readonly LineDecoration[], b: readonly LineDecoration[]): boolean {
const aLen = a.length;
const bLen = b.length;
if (aLen !== bLen) {
@@ -8,7 +8,7 @@ import { renderIcon } from '../../../../../../base/browser/ui/iconLabel/iconLabe
import { Codicon } from '../../../../../../base/common/codicons.js';
import { Emitter, Event } from '../../../../../../base/common/event.js';
import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js';
import { IObservable, autorun, autorunWithStore, constObservable, derived, observableSignalFromEvent, observableValue } from '../../../../../../base/common/observable.js';
import { IObservable, autorun, autorunWithStore, constObservable, derived, derivedOpts, observableSignalFromEvent, observableValue } from '../../../../../../base/common/observable.js';
import * as strings from '../../../../../../base/common/strings.js';
import { applyFontInfo } from '../../../../../browser/config/domFontInfo.js';
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidgetPosition, IViewZoneChangeAccessor, MouseTargetType } from '../../../../../browser/editorBrowser.js';
@@ -34,7 +34,8 @@ import { CodeEditorWidget } from '../../../../../browser/widget/codeEditor/codeE
import { TokenWithTextArray } from '../../../../../common/tokens/tokenWithTextArray.js';
import { InlineCompletionViewData } from '../inlineEdits/inlineEditsViewInterface.js';
import { InlineDecorationType } from '../../../../../common/viewModel/inlineDecorations.js';
import { sum } from '../../../../../../base/common/arrays.js';
import { equals, sum } from '../../../../../../base/common/arrays.js';
import { equalsIfDefined, IEquatable, itemEquals } from '../../../../../../base/common/equals.js';
export interface IGhostTextWidgetData {
readonly ghostText: GhostText | GhostTextReplacement;
@@ -102,14 +103,14 @@ export class GhostTextView extends Disposable {
this._additionalLinesWidget = this._register(
new AdditionalLinesWidget(
this._editor,
derived(reader => {
derivedOpts({ owner: this, equalsFn: equalsIfDefined(itemEquals()) }, reader => {
/** @description lines */
const uiState = this._state.read(reader);
return uiState ? {
lineNumber: uiState.lineNumber,
additionalLines: uiState.additionalLines,
minReservedLineCount: uiState.additionalReservedLineCount,
} : undefined;
return uiState ? new AdditionalLinesData(
uiState.lineNumber,
uiState.additionalLines,
uiState.additionalReservedLineCount,
) : undefined;
}),
this._shouldKeepCursorStable,
this._isClickable
@@ -243,10 +244,10 @@ export class GhostTextView extends Disposable {
const existingContent = t.slice(additionalLinesOriginalSuffix.columnRange.toZeroBasedOffsetRange());
content = TokenWithTextArray.fromLineTokens(content).append(existingContent).toLineTokens(content.languageIdCodec);
}
return {
return new LineData(
content,
decorations: l.decorations,
};
l.decorations,
);
});
const cursorColumn = this._editor.getSelection()?.getStartPosition().column!;
@@ -420,6 +421,24 @@ function computeGhostTextViewData(ghostText: GhostText | GhostTextReplacement, t
};
}
class AdditionalLinesData implements IEquatable<AdditionalLinesData> {
constructor(
public readonly lineNumber: number,
public readonly additionalLines: readonly LineData[],
public readonly minReservedLineCount: number,
) { }
equals(other: AdditionalLinesData): boolean {
if (this.lineNumber !== other.lineNumber) {
return false;
}
if (this.minReservedLineCount !== other.minReservedLineCount) {
return false;
}
return equals(this.additionalLines, other.additionalLines, itemEquals());
}
}
export class AdditionalLinesWidget extends Disposable {
private _viewZoneInfo: { viewZoneId: string; heightInLines: number; lineNumber: number } | undefined;
public get viewZoneId(): string | undefined { return this._viewZoneInfo?.viewZoneId; }
@@ -440,11 +459,7 @@ export class AdditionalLinesWidget extends Disposable {
constructor(
private readonly _editor: ICodeEditor,
private readonly _lines: IObservable<{
lineNumber: number;
additionalLines: LineData[];
minReservedLineCount: number;
} | undefined>,
private readonly _lines: IObservable<AdditionalLinesData | undefined>,
private readonly _shouldKeepCursorStable: boolean,
private readonly _isClickable: boolean,
) {
@@ -500,7 +515,7 @@ export class AdditionalLinesWidget extends Disposable {
});
}
private updateLines(lineNumber: number, additionalLines: LineData[], minReservedLineCount: number): void {
private updateLines(lineNumber: number, additionalLines: readonly LineData[], minReservedLineCount: number): void {
const textModel = this._editor.getModel();
if (!textModel) {
return;
@@ -581,12 +596,21 @@ function isTargetGhostText(target: EventTarget | null): boolean {
return isHTMLElement(target) && target.classList.contains(GHOST_TEXT_CLASS_NAME);
}
export interface LineData {
content: LineTokens; // Must not contain a linebreak!
decorations: LineDecoration[];
export class LineData implements IEquatable<LineData> {
constructor(
public readonly content: LineTokens, // Must not contain a linebreak!
public readonly decorations: readonly LineDecoration[]
) { }
equals(other: LineData): boolean {
if (!this.content.equals(other.content)) {
return false;
}
return LineDecoration.equalsArr(this.decorations, other.decorations);
}
}
function renderLines(domNode: HTMLElement, tabSize: number, lines: LineData[], opts: IComputedEditorOptions, isClickable: boolean): void {
function renderLines(domNode: HTMLElement, tabSize: number, lines: readonly LineData[], opts: IComputedEditorOptions, isClickable: boolean): void {
const disableMonospaceOptimizations = opts.get(EditorOption.disableMonospaceOptimizations);
const stopRenderingLineAfter = opts.get(EditorOption.stopRenderingLineAfter);
// To avoid visual confusion, we don't want to render visible whitespace
@@ -625,7 +649,7 @@ function renderLines(domNode: HTMLElement, tabSize: number, lines: LineData[], o
containsRTL,
0,
lineTokens,
lineData.decorations,
lineData.decorations.slice(),
tabSize,
0,
fontInfo.spaceWidth,