Path: blob/main/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { isHTMLElement, ModifierKeyEmitter } from '../../../../base/browser/dom.js';6import { isNonEmptyArray } from '../../../../base/common/arrays.js';7import { disposableTimeout, RunOnceScheduler } from '../../../../base/common/async.js';8import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';9import { onUnexpectedError } from '../../../../base/common/errors.js';10import { DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';11import { LRUCache } from '../../../../base/common/map.js';12import { IRange } from '../../../../base/common/range.js';13import { assertType } from '../../../../base/common/types.js';14import { URI } from '../../../../base/common/uri.js';15import { IActiveCodeEditor, ICodeEditor, IEditorMouseEvent, MouseTargetType } from '../../../browser/editorBrowser.js';16import { ClassNameReference, CssProperties, DynamicCssRules } from '../../../browser/editorDom.js';17import { StableEditorScrollState } from '../../../browser/stableEditorScroll.js';18import { EditorOption, EDITOR_FONT_DEFAULTS } from '../../../common/config/editorOptions.js';19import { EditOperation } from '../../../common/core/editOperation.js';20import { Range } from '../../../common/core/range.js';21import { IEditorContribution } from '../../../common/editorCommon.js';22import * as languages from '../../../common/languages.js';23import { IModelDeltaDecoration, InjectedTextCursorStops, InjectedTextOptions, ITextModel, TrackedRangeStickiness } from '../../../common/model.js';24import { ModelDecorationInjectedTextOptions } from '../../../common/model/textModel.js';25import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from '../../../common/services/languageFeatureDebounce.js';26import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';27import { ITextModelService } from '../../../common/services/resolverService.js';28import { ClickLinkGesture, ClickLinkMouseEvent } from '../../gotoSymbol/browser/link/clickLinkGesture.js';29import { InlayHintAnchor, InlayHintItem, InlayHintsFragments } from './inlayHints.js';30import { goToDefinitionWithLocation, showGoToContextMenu } from './inlayHintsLocations.js';31import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js';32import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';33import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';34import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';35import * as colors from '../../../../platform/theme/common/colorRegistry.js';36import { themeColorFromId } from '../../../../platform/theme/common/themeService.js';37import { Position } from '../../../common/core/position.js';3839// --- hint caching service (per session)4041class InlayHintsCache {4243declare readonly _serviceBrand: undefined;4445private readonly _entries = new LRUCache<string, InlayHintItem[]>(50);4647get(model: ITextModel): InlayHintItem[] | undefined {48const key = InlayHintsCache._key(model);49return this._entries.get(key);50}5152set(model: ITextModel, value: InlayHintItem[]): void {53const key = InlayHintsCache._key(model);54this._entries.set(key, value);55}5657private static _key(model: ITextModel): string {58return `${model.uri.toString()}/${model.getVersionId()}`;59}60}6162interface IInlayHintsCache extends InlayHintsCache { }63const IInlayHintsCache = createDecorator<IInlayHintsCache>('IInlayHintsCache');64registerSingleton(IInlayHintsCache, InlayHintsCache, InstantiationType.Delayed);6566// --- rendered label6768export class RenderedInlayHintLabelPart {69constructor(readonly item: InlayHintItem, readonly index: number) { }7071get part() {72const label = this.item.hint.label;73if (typeof label === 'string') {74return { label };75} else {76return label[this.index];77}78}79}8081class ActiveInlayHintInfo {82constructor(readonly part: RenderedInlayHintLabelPart, readonly hasTriggerModifier: boolean) { }83}8485type InlayHintDecorationRenderInfo = {86item: InlayHintItem;87decoration: IModelDeltaDecoration;88classNameRef: ClassNameReference;89};9091const enum RenderMode {92Normal,93Invisible94}9596// --- controller9798export class InlayHintsController implements IEditorContribution {99100static readonly ID: string = 'editor.contrib.InlayHints';101102private static readonly _MAX_DECORATORS = 1500;103private static readonly _whitespaceData = {};104105static get(editor: ICodeEditor): InlayHintsController | undefined {106return editor.getContribution<InlayHintsController>(InlayHintsController.ID) ?? undefined;107}108109private readonly _disposables = new DisposableStore();110private readonly _sessionDisposables = new DisposableStore();111private readonly _decorationsMetadata = new Map<string, InlayHintDecorationRenderInfo>();112private readonly _debounceInfo: IFeatureDebounceInformation;113private readonly _ruleFactory: DynamicCssRules;114115private _cursorInfo?: { position: Position; notEarlierThan: number };116private _activeRenderMode = RenderMode.Normal;117private _activeInlayHintPart?: ActiveInlayHintInfo;118119constructor(120private readonly _editor: ICodeEditor,121@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,122@ILanguageFeatureDebounceService _featureDebounce: ILanguageFeatureDebounceService,123@IInlayHintsCache private readonly _inlayHintsCache: IInlayHintsCache,124@ICommandService private readonly _commandService: ICommandService,125@INotificationService private readonly _notificationService: INotificationService,126@IInstantiationService private readonly _instaService: IInstantiationService,127) {128this._ruleFactory = this._disposables.add(new DynamicCssRules(this._editor));129this._debounceInfo = _featureDebounce.for(_languageFeaturesService.inlayHintsProvider, 'InlayHint', { min: 25 });130this._disposables.add(_languageFeaturesService.inlayHintsProvider.onDidChange(() => this._update()));131this._disposables.add(_editor.onDidChangeModel(() => this._update()));132this._disposables.add(_editor.onDidChangeModelLanguage(() => this._update()));133this._disposables.add(_editor.onDidChangeConfiguration(e => {134if (e.hasChanged(EditorOption.inlayHints)) {135this._update();136}137}));138this._update();139140}141142dispose(): void {143this._sessionDisposables.dispose();144this._removeAllDecorations();145this._disposables.dispose();146}147148private _update(): void {149this._sessionDisposables.clear();150this._removeAllDecorations();151152const options = this._editor.getOption(EditorOption.inlayHints);153if (options.enabled === 'off') {154return;155}156157const model = this._editor.getModel();158if (!model || !this._languageFeaturesService.inlayHintsProvider.has(model)) {159return;160}161162if (options.enabled === 'on') {163// different "on" modes: always164this._activeRenderMode = RenderMode.Normal;165} else {166// different "on" modes: offUnlessPressed, or onUnlessPressed167let defaultMode: RenderMode;168let altMode: RenderMode;169if (options.enabled === 'onUnlessPressed') {170defaultMode = RenderMode.Normal;171altMode = RenderMode.Invisible;172} else {173defaultMode = RenderMode.Invisible;174altMode = RenderMode.Normal;175}176this._activeRenderMode = defaultMode;177178this._sessionDisposables.add(ModifierKeyEmitter.getInstance().event(e => {179if (!this._editor.hasModel()) {180return;181}182const newRenderMode = e.altKey && e.ctrlKey && !(e.shiftKey || e.metaKey) ? altMode : defaultMode;183if (newRenderMode !== this._activeRenderMode) {184this._activeRenderMode = newRenderMode;185const model = this._editor.getModel();186const copies = this._copyInlayHintsWithCurrentAnchor(model);187this._updateHintsDecorators([model.getFullModelRange()], copies);188scheduler.schedule(0);189}190}));191}192193// iff possible, quickly update from cache194const cached = this._inlayHintsCache.get(model);195if (cached) {196this._updateHintsDecorators([model.getFullModelRange()], cached);197}198this._sessionDisposables.add(toDisposable(() => {199// cache items when switching files etc200if (!model.isDisposed()) {201this._cacheHintsForFastRestore(model);202}203}));204205let cts: CancellationTokenSource | undefined;206const watchedProviders = new Set<languages.InlayHintsProvider>();207208const scheduler = new RunOnceScheduler(async () => {209const t1 = Date.now();210211cts?.dispose(true);212cts = new CancellationTokenSource();213const listener = model.onWillDispose(() => cts?.cancel());214215try {216const myToken = cts.token;217const inlayHints = await InlayHintsFragments.create(this._languageFeaturesService.inlayHintsProvider, model, this._getHintsRanges(), myToken);218scheduler.delay = this._debounceInfo.update(model, Date.now() - t1);219if (myToken.isCancellationRequested) {220inlayHints.dispose();221return;222}223224// listen to provider changes225for (const provider of inlayHints.provider) {226if (typeof provider.onDidChangeInlayHints === 'function' && !watchedProviders.has(provider)) {227watchedProviders.add(provider);228this._sessionDisposables.add(provider.onDidChangeInlayHints(() => {229if (!scheduler.isScheduled()) { // ignore event when request is already scheduled230scheduler.schedule();231}232}));233}234}235236this._sessionDisposables.add(inlayHints);237this._updateHintsDecorators(inlayHints.ranges, inlayHints.items);238this._cacheHintsForFastRestore(model);239240} catch (err) {241onUnexpectedError(err);242243} finally {244cts.dispose();245listener.dispose();246}247248}, this._debounceInfo.get(model));249250this._sessionDisposables.add(scheduler);251this._sessionDisposables.add(toDisposable(() => cts?.dispose(true)));252scheduler.schedule(0);253254this._sessionDisposables.add(this._editor.onDidScrollChange((e) => {255// update when scroll position changes256// uses scrollTopChanged has weak heuristic to differenatiate between scrolling due to257// typing or due to "actual" scrolling258if (e.scrollTopChanged || !scheduler.isScheduled()) {259scheduler.schedule();260}261}));262263const cursor = this._sessionDisposables.add(new MutableDisposable());264this._sessionDisposables.add(this._editor.onDidChangeModelContent((e) => {265cts?.cancel();266267// mark current cursor position and time after which the whole can be updated/redrawn268const delay = Math.max(scheduler.delay, 800);269this._cursorInfo = { position: this._editor.getPosition()!, notEarlierThan: Date.now() + delay };270cursor.value = disposableTimeout(() => scheduler.schedule(0), delay);271272scheduler.schedule();273}));274275this._sessionDisposables.add(this._editor.onDidChangeConfiguration(e => {276if (e.hasChanged(EditorOption.inlayHints)) {277scheduler.schedule();278}279}));280281// mouse gestures282this._sessionDisposables.add(this._installDblClickGesture(() => scheduler.schedule(0)));283this._sessionDisposables.add(this._installLinkGesture());284this._sessionDisposables.add(this._installContextMenu());285}286287private _installLinkGesture(): IDisposable {288289const store = new DisposableStore();290const gesture = store.add(new ClickLinkGesture(this._editor));291292// let removeHighlight = () => { };293294const sessionStore = new DisposableStore();295store.add(sessionStore);296297store.add(gesture.onMouseMoveOrRelevantKeyDown(e => {298const [mouseEvent] = e;299const labelPart = this._getInlayHintLabelPart(mouseEvent);300const model = this._editor.getModel();301302if (!labelPart || !model) {303sessionStore.clear();304return;305}306307// resolve the item308const cts = new CancellationTokenSource();309sessionStore.add(toDisposable(() => cts.dispose(true)));310labelPart.item.resolve(cts.token);311312// render link => when the modifier is pressed and when there is a command or location313this._activeInlayHintPart = labelPart.part.command || labelPart.part.location314? new ActiveInlayHintInfo(labelPart, mouseEvent.hasTriggerModifier)315: undefined;316317const lineNumber = model.validatePosition(labelPart.item.hint.position).lineNumber;318const range = new Range(lineNumber, 1, lineNumber, model.getLineMaxColumn(lineNumber));319const lineHints = this._getInlineHintsForRange(range);320this._updateHintsDecorators([range], lineHints);321sessionStore.add(toDisposable(() => {322this._activeInlayHintPart = undefined;323this._updateHintsDecorators([range], lineHints);324}));325}));326store.add(gesture.onCancel(() => sessionStore.clear()));327store.add(gesture.onExecute(async e => {328const label = this._getInlayHintLabelPart(e);329if (label) {330const part = label.part;331if (part.location) {332// location -> execute go to def333this._instaService.invokeFunction(goToDefinitionWithLocation, e, this._editor as IActiveCodeEditor, part.location);334} else if (languages.Command.is(part.command)) {335// command -> execute it336await this._invokeCommand(part.command, label.item);337}338}339}));340return store;341}342343private _getInlineHintsForRange(range: Range) {344const lineHints = new Set<InlayHintItem>();345for (const data of this._decorationsMetadata.values()) {346if (range.containsRange(data.item.anchor.range)) {347lineHints.add(data.item);348}349}350return Array.from(lineHints);351}352353private _installDblClickGesture(updateInlayHints: Function): IDisposable {354return this._editor.onMouseUp(async e => {355if (e.event.detail !== 2) {356return;357}358const part = this._getInlayHintLabelPart(e);359if (!part) {360return;361}362e.event.preventDefault();363await part.item.resolve(CancellationToken.None);364if (isNonEmptyArray(part.item.hint.textEdits)) {365const edits = part.item.hint.textEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text));366this._editor.executeEdits('inlayHint.default', edits);367updateInlayHints();368}369});370}371372private _installContextMenu(): IDisposable {373return this._editor.onContextMenu(async e => {374if (!(isHTMLElement(e.event.target))) {375return;376}377const part = this._getInlayHintLabelPart(e);378if (part) {379await this._instaService.invokeFunction(showGoToContextMenu, this._editor, e.event.target, part);380}381});382}383384private _getInlayHintLabelPart(e: IEditorMouseEvent | ClickLinkMouseEvent): RenderedInlayHintLabelPart | undefined {385if (e.target.type !== MouseTargetType.CONTENT_TEXT) {386return undefined;387}388const options = e.target.detail.injectedText?.options;389if (options instanceof ModelDecorationInjectedTextOptions && options?.attachedData instanceof RenderedInlayHintLabelPart) {390return options.attachedData;391}392return undefined;393}394395private async _invokeCommand(command: languages.Command, item: InlayHintItem) {396try {397await this._commandService.executeCommand(command.id, ...(command.arguments ?? []));398} catch (err) {399this._notificationService.notify({400severity: Severity.Error,401source: item.provider.displayName,402message: err403});404}405}406407private _cacheHintsForFastRestore(model: ITextModel): void {408const hints = this._copyInlayHintsWithCurrentAnchor(model);409this._inlayHintsCache.set(model, hints);410}411412// return inlay hints but with an anchor that reflects "updates"413// that happened after receiving them, e.g adding new lines before a hint414private _copyInlayHintsWithCurrentAnchor(model: ITextModel): InlayHintItem[] {415const items = new Map<InlayHintItem, InlayHintItem>();416for (const [id, obj] of this._decorationsMetadata) {417if (items.has(obj.item)) {418// an inlay item can be rendered as multiple decorations419// but they will all uses the same range420continue;421}422const range = model.getDecorationRange(id);423if (range) {424// update range with whatever the editor has tweaked it to425const anchor = new InlayHintAnchor(range, obj.item.anchor.direction);426const copy = obj.item.with({ anchor });427items.set(obj.item, copy);428}429}430return Array.from(items.values());431}432433private _getHintsRanges(): Range[] {434const extra = 30;435const model = this._editor.getModel()!;436const visibleRanges = this._editor.getVisibleRangesPlusViewportAboveBelow();437const result: Range[] = [];438for (const range of visibleRanges.sort(Range.compareRangesUsingStarts)) {439const extendedRange = model.validateRange(new Range(range.startLineNumber - extra, range.startColumn, range.endLineNumber + extra, range.endColumn));440if (result.length === 0 || !Range.areIntersectingOrTouching(result[result.length - 1], extendedRange)) {441result.push(extendedRange);442} else {443result[result.length - 1] = Range.plusRange(result[result.length - 1], extendedRange);444}445}446return result;447}448449private _updateHintsDecorators(ranges: readonly Range[], items: readonly InlayHintItem[]): void {450451const itemFixedLengths = new Map<InlayHintItem, number>();452453if (this._cursorInfo454&& this._cursorInfo.notEarlierThan > Date.now()455&& ranges.some(range => range.containsPosition(this._cursorInfo!.position))456) {457// collect inlay hints that are on the same line and before the cursor. Those "old" hints458// define fixed lengths so that the cursor does not jump back and worth while typing.459const { position } = this._cursorInfo;460this._cursorInfo = undefined;461462const lengths = new Map<InlayHintItem, number>();463464for (const deco of this._editor.getLineDecorations(position.lineNumber) ?? []) {465466const data = this._decorationsMetadata.get(deco.id);467if (deco.range.startColumn > position.column) {468continue;469}470const opts = data?.decoration.options[data.item.anchor.direction];471if (opts && opts.attachedData !== InlayHintsController._whitespaceData) {472const len = lengths.get(data.item) ?? 0;473lengths.set(data.item, len + opts.content.length);474}475}476477478// on the cursor line and before the cursor-column479const newItemsWithFixedLength = items.filter(item => item.anchor.range.startLineNumber === position.lineNumber && item.anchor.range.endColumn <= position.column);480const fixedLengths = Array.from(lengths.values());481482// match up fixed lengths with items and distribute the remaining lengths to the last item483let lastItem: InlayHintItem | undefined;484while (true) {485const targetItem = newItemsWithFixedLength.shift();486const fixedLength = fixedLengths.shift();487488if (!fixedLength && !targetItem) {489break; // DONE490}491492if (targetItem) {493itemFixedLengths.set(targetItem, fixedLength ?? 0);494lastItem = targetItem;495496} else if (lastItem && fixedLength) {497// still lengths but no more item. give it all to the last498let len = itemFixedLengths.get(lastItem)!;499len += fixedLength;500len += fixedLengths.reduce((p, c) => p + c, 0);501fixedLengths.length = 0;502break; // DONE503}504}505}506507// utils to collect/create injected text decorations508const newDecorationsData: InlayHintDecorationRenderInfo[] = [];509const addInjectedText = (item: InlayHintItem, ref: ClassNameReference, content: string, cursorStops: InjectedTextCursorStops, attachedData?: RenderedInlayHintLabelPart | object): void => {510const opts: InjectedTextOptions = {511content,512inlineClassNameAffectsLetterSpacing: true,513inlineClassName: ref.className,514cursorStops,515attachedData516};517newDecorationsData.push({518item,519classNameRef: ref,520decoration: {521range: item.anchor.range,522options: {523// className: "rangeHighlight", // DEBUG highlight to see to what range a hint is attached524description: 'InlayHint',525showIfCollapsed: item.anchor.range.isEmpty(), // "original" range is empty526collapseOnReplaceEdit: !item.anchor.range.isEmpty(),527stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,528[item.anchor.direction]: this._activeRenderMode === RenderMode.Normal ? opts : undefined529}530}531});532};533534const addInjectedWhitespace = (item: InlayHintItem, isLast: boolean): void => {535const marginRule = this._ruleFactory.createClassNameRef({536width: `${(fontSize / 3) | 0}px`,537display: 'inline-block'538});539addInjectedText(item, marginRule, '\u200a', isLast ? InjectedTextCursorStops.Right : InjectedTextCursorStops.None, InlayHintsController._whitespaceData);540};541542543//544const { fontSize, fontFamily, padding, isUniform } = this._getLayoutInfo();545const maxLength = this._editor.getOption(EditorOption.inlayHints).maximumLength;546const fontFamilyVar = '--code-editorInlayHintsFontFamily';547this._editor.getContainerDomNode().style.setProperty(fontFamilyVar, fontFamily);548549550type ILineInfo = { line: number; totalLen: number };551let currentLineInfo: ILineInfo = { line: 0, totalLen: 0 };552553for (let i = 0; i < items.length; i++) {554const item = items[i];555556if (currentLineInfo.line !== item.anchor.range.startLineNumber) {557currentLineInfo = { line: item.anchor.range.startLineNumber, totalLen: 0 };558}559560if (maxLength && currentLineInfo.totalLen > maxLength) {561continue;562}563564// whitespace leading the actual label565if (item.hint.paddingLeft) {566addInjectedWhitespace(item, false);567}568569// the label with its parts570const parts: languages.InlayHintLabelPart[] = typeof item.hint.label === 'string'571? [{ label: item.hint.label }]572: item.hint.label;573574const itemFixedLength = itemFixedLengths.get(item);575let itemActualLength = 0;576577for (let i = 0; i < parts.length; i++) {578const part = parts[i];579580const isFirst = i === 0;581const isLast = i === parts.length - 1;582583const cssProperties: CssProperties = {584fontSize: `${fontSize}px`,585fontFamily: `var(${fontFamilyVar}), ${EDITOR_FONT_DEFAULTS.fontFamily}`,586verticalAlign: isUniform ? 'baseline' : 'middle',587unicodeBidi: 'isolate'588};589590if (isNonEmptyArray(item.hint.textEdits)) {591cssProperties.cursor = 'default';592}593594this._fillInColors(cssProperties, item.hint);595596if ((part.command || part.location) && this._activeInlayHintPart?.part.item === item && this._activeInlayHintPart.part.index === i) {597// active link!598cssProperties.textDecoration = 'underline';599if (this._activeInlayHintPart.hasTriggerModifier) {600cssProperties.color = themeColorFromId(colors.editorActiveLinkForeground);601cssProperties.cursor = 'pointer';602}603}604605let textlabel = part.label;606currentLineInfo.totalLen += textlabel.length;607let tooLong = false;608const over = maxLength !== 0 ? (currentLineInfo.totalLen - maxLength) : 0;609if (over > 0) {610textlabel = textlabel.slice(0, -over) + '…';611tooLong = true;612}613614itemActualLength += textlabel.length;615616if (itemFixedLength !== undefined) {617const overFixedLength = itemActualLength - itemFixedLength;618if (overFixedLength >= 0) {619// longer than fixed length, trim620itemActualLength -= overFixedLength;621textlabel = textlabel.slice(0, -(1 + overFixedLength)) + '…';622tooLong = true;623}624}625626if (padding) {627if (isFirst && (isLast || tooLong)) {628// only element629cssProperties.padding = `1px ${Math.max(1, fontSize / 4) | 0}px`;630cssProperties.borderRadius = `${(fontSize / 4) | 0}px`;631} else if (isFirst) {632// first element633cssProperties.padding = `1px 0 1px ${Math.max(1, fontSize / 4) | 0}px`;634cssProperties.borderRadius = `${(fontSize / 4) | 0}px 0 0 ${(fontSize / 4) | 0}px`;635} else if ((isLast || tooLong)) {636// last element637cssProperties.padding = `1px ${Math.max(1, fontSize / 4) | 0}px 1px 0`;638cssProperties.borderRadius = `0 ${(fontSize / 4) | 0}px ${(fontSize / 4) | 0}px 0`;639} else {640cssProperties.padding = `1px 0 1px 0`;641}642}643644addInjectedText(645item,646this._ruleFactory.createClassNameRef(cssProperties),647fixSpace(textlabel),648isLast && !item.hint.paddingRight ? InjectedTextCursorStops.Right : InjectedTextCursorStops.None,649new RenderedInlayHintLabelPart(item, i)650);651652if (tooLong) {653break;654}655}656657if (itemFixedLength !== undefined && itemActualLength < itemFixedLength) {658// shorter than fixed length, pad659const pad = (itemFixedLength - itemActualLength);660addInjectedText(661item,662this._ruleFactory.createClassNameRef({}),663'\u200a'.repeat(pad),664InjectedTextCursorStops.None665);666}667668// whitespace trailing the actual label669if (item.hint.paddingRight) {670addInjectedWhitespace(item, true);671}672673if (newDecorationsData.length > InlayHintsController._MAX_DECORATORS) {674break;675}676}677678// collect all decoration ids that are affected by the ranges679// and only update those decorations680const decorationIdsToReplace: string[] = [];681for (const [id, metadata] of this._decorationsMetadata) {682const range = this._editor.getModel()?.getDecorationRange(id);683if (range && ranges.some(r => r.containsRange(range))) {684decorationIdsToReplace.push(id);685metadata.classNameRef.dispose();686this._decorationsMetadata.delete(id);687}688}689690const scrollState = StableEditorScrollState.capture(this._editor);691692this._editor.changeDecorations(accessor => {693const newDecorationIds = accessor.deltaDecorations(decorationIdsToReplace, newDecorationsData.map(d => d.decoration));694for (let i = 0; i < newDecorationIds.length; i++) {695const data = newDecorationsData[i];696this._decorationsMetadata.set(newDecorationIds[i], data);697}698});699700scrollState.restore(this._editor);701}702703private _fillInColors(props: CssProperties, hint: languages.InlayHint): void {704if (hint.kind === languages.InlayHintKind.Parameter) {705props.backgroundColor = themeColorFromId(colors.editorInlayHintParameterBackground);706props.color = themeColorFromId(colors.editorInlayHintParameterForeground);707} else if (hint.kind === languages.InlayHintKind.Type) {708props.backgroundColor = themeColorFromId(colors.editorInlayHintTypeBackground);709props.color = themeColorFromId(colors.editorInlayHintTypeForeground);710} else {711props.backgroundColor = themeColorFromId(colors.editorInlayHintBackground);712props.color = themeColorFromId(colors.editorInlayHintForeground);713}714}715716private _getLayoutInfo() {717const options = this._editor.getOption(EditorOption.inlayHints);718const padding = options.padding;719720const editorFontSize = this._editor.getOption(EditorOption.fontSize);721const editorFontFamily = this._editor.getOption(EditorOption.fontFamily);722723let fontSize = options.fontSize;724if (!fontSize || fontSize < 5 || fontSize > editorFontSize) {725fontSize = editorFontSize;726}727728const fontFamily = options.fontFamily || editorFontFamily;729730const isUniform = !padding731&& fontFamily === editorFontFamily732&& fontSize === editorFontSize;733734return { fontSize, fontFamily, padding, isUniform };735}736737private _removeAllDecorations(): void {738this._editor.removeDecorations(Array.from(this._decorationsMetadata.keys()));739for (const obj of this._decorationsMetadata.values()) {740obj.classNameRef.dispose();741}742this._decorationsMetadata.clear();743}744745746// --- accessibility747748getInlayHintsForLine(line: number): InlayHintItem[] {749if (!this._editor.hasModel()) {750return [];751}752const set = new Set<languages.InlayHint>();753const result: InlayHintItem[] = [];754for (const deco of this._editor.getLineDecorations(line)) {755const data = this._decorationsMetadata.get(deco.id);756if (data && !set.has(data.item.hint)) {757set.add(data.item.hint);758result.push(data.item);759}760}761return result;762}763}764765766// Prevents the view from potentially visible whitespace767function fixSpace(str: string): string {768const noBreakWhitespace = '\xa0';769return str.replace(/[ \t]/g, noBreakWhitespace);770}771772CommandsRegistry.registerCommand('_executeInlayHintProvider', async (accessor, ...args: [URI, IRange]): Promise<languages.InlayHint[]> => {773774const [uri, range] = args;775assertType(URI.isUri(uri));776assertType(Range.isIRange(range));777778const { inlayHintsProvider } = accessor.get(ILanguageFeaturesService);779const ref = await accessor.get(ITextModelService).createModelReference(uri);780try {781const model = await InlayHintsFragments.create(inlayHintsProvider, ref.object.textEditorModel, [Range.lift(range)], CancellationToken.None);782const result = model.items.map(i => i.hint);783setTimeout(() => model.dispose(), 0); // dispose after sending to ext host784return result;785} finally {786ref.dispose();787}788});789790791