Path: blob/main/src/vs/editor/contrib/inlayHints/browser/inlayHints.ts
5283 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 { CancellationToken } from '../../../../base/common/cancellation.js';6import { CancellationError, onUnexpectedExternalError } from '../../../../base/common/errors.js';7import { DisposableStore } from '../../../../base/common/lifecycle.js';8import { IPosition, Position } from '../../../common/core/position.js';9import { Range } from '../../../common/core/range.js';10import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js';11import { InlayHint, InlayHintList, InlayHintsProvider, Command } from '../../../common/languages.js';12import { ITextModel } from '../../../common/model.js';13import { createCommandUri } from '../../../../base/common/htmlContent.js';1415export class InlayHintAnchor {16constructor(readonly range: Range, readonly direction: 'before' | 'after') { }17}1819export class InlayHintItem {2021private _isResolved: boolean = false;22private _currentResolve?: Promise<void>;2324constructor(readonly hint: InlayHint, readonly anchor: InlayHintAnchor, readonly provider: InlayHintsProvider) { }2526with(delta: { anchor: InlayHintAnchor }): InlayHintItem {27const result = new InlayHintItem(this.hint, delta.anchor, this.provider);28result._isResolved = this._isResolved;29result._currentResolve = this._currentResolve;30return result;31}3233async resolve(token: CancellationToken): Promise<void> {34if (typeof this.provider.resolveInlayHint !== 'function') {35return;36}37if (this._currentResolve) {38// wait for an active resolve operation and try again39// when that's done.40await this._currentResolve;41if (token.isCancellationRequested) {42return;43}44return this.resolve(token);45}46if (!this._isResolved) {47this._currentResolve = this._doResolve(token)48.finally(() => this._currentResolve = undefined);49}50await this._currentResolve;51}5253private async _doResolve(token: CancellationToken) {54try {55const newHint = await Promise.resolve(this.provider.resolveInlayHint!(this.hint, token));56this.hint.tooltip = newHint?.tooltip ?? this.hint.tooltip;57this.hint.label = newHint?.label ?? this.hint.label;58this.hint.textEdits = newHint?.textEdits ?? this.hint.textEdits;59this._isResolved = true;60} catch (err) {61onUnexpectedExternalError(err);62this._isResolved = false;63}64}65}6667export class InlayHintsFragments {6869private static _emptyInlayHintList: InlayHintList = Object.freeze({ dispose() { }, hints: [] });7071static async create(registry: LanguageFeatureRegistry<InlayHintsProvider>, model: ITextModel, ranges: Range[], token: CancellationToken): Promise<InlayHintsFragments> {7273const data: [InlayHintList, InlayHintsProvider][] = [];7475const promises = registry.ordered(model).reverse().map(provider => ranges.map(async range => {76try {77const result = await provider.provideInlayHints(model, range, token);78if (result?.hints.length || provider.onDidChangeInlayHints) {79data.push([result ?? InlayHintsFragments._emptyInlayHintList, provider]);80}81} catch (err) {82onUnexpectedExternalError(err);83}84}));8586await Promise.all(promises.flat());8788if (token.isCancellationRequested || model.isDisposed()) {89throw new CancellationError();90}9192return new InlayHintsFragments(ranges, data, model);93}9495private readonly _disposables = new DisposableStore();9697readonly items: readonly InlayHintItem[];98readonly ranges: readonly Range[];99readonly provider: Set<InlayHintsProvider>;100101private constructor(ranges: Range[], data: [InlayHintList, InlayHintsProvider][], model: ITextModel) {102this.ranges = ranges;103this.provider = new Set();104const items: InlayHintItem[] = [];105for (const [list, provider] of data) {106this._disposables.add(list);107this.provider.add(provider);108109for (const hint of list.hints) {110// compute the range to which the item should be attached to111const position = model.validatePosition(hint.position);112let direction: 'before' | 'after' = 'before';113114const wordRange = InlayHintsFragments._getRangeAtPosition(model, position);115let range: Range;116117if (wordRange.getStartPosition().isBefore(position)) {118range = Range.fromPositions(wordRange.getStartPosition(), position);119direction = 'after';120} else {121range = Range.fromPositions(position, wordRange.getEndPosition());122direction = 'before';123}124125items.push(new InlayHintItem(hint, new InlayHintAnchor(range, direction), provider));126}127}128this.items = items.sort((a, b) => Position.compare(a.hint.position, b.hint.position));129}130131dispose(): void {132this._disposables.dispose();133}134135private static _getRangeAtPosition(model: ITextModel, position: IPosition): Range {136const line = position.lineNumber;137const word = model.getWordAtPosition(position);138if (word) {139// always prefer the word range140return new Range(line, word.startColumn, line, word.endColumn);141}142143model.tokenization.tokenizeIfCheap(line);144const tokens = model.tokenization.getLineTokens(line);145const offset = position.column - 1;146const idx = tokens.findTokenIndexAtOffset(offset);147148let start = tokens.getStartOffset(idx);149let end = tokens.getEndOffset(idx);150151if (end - start === 1) {152// single character token, when at its end try leading/trailing token instead153if (start === offset && idx > 1) {154// leading token155start = tokens.getStartOffset(idx - 1);156end = tokens.getEndOffset(idx - 1);157} else if (end === offset && idx < tokens.getCount() - 1) {158// trailing token159start = tokens.getStartOffset(idx + 1);160end = tokens.getEndOffset(idx + 1);161}162}163164return new Range(line, start + 1, line, end + 1);165}166}167168export function asCommandLink(command: Command): string {169return createCommandUri(command.id, ...(command.arguments ?? [])).toString();170}171172173