From 7d640ead7ba93977530733ce3a1e8cc96d2976fb Mon Sep 17 00:00:00 2001 From: vince-fugnitto Date: Mon, 3 Oct 2022 11:09:30 -0400 Subject: [PATCH] vscode: add `InlayHints` support Signed-off-by: vince-fugnitto --- packages/plugin-ext/src/common/cache.ts | 51 +++++ .../src/common/plugin-api-rpc-model.ts | 32 +++ .../plugin-ext/src/common/plugin-api-rpc.ts | 13 +- .../src/main/browser/languages-main.ts | 87 ++++++++- .../plugin-ext/src/plugin/custom-editors.ts | 34 +--- packages/plugin-ext/src/plugin/languages.ts | 33 ++++ .../src/plugin/languages/inlay-hints.ts | 149 ++++++++++++++ .../plugin-ext/src/plugin/plugin-context.ts | 13 +- .../plugin-ext/src/plugin/type-converters.ts | 10 +- packages/plugin-ext/src/plugin/types-impl.ts | 34 ++++ packages/plugin/src/theia.d.ts | 183 ++++++++++++++++++ 11 files changed, 600 insertions(+), 39 deletions(-) create mode 100644 packages/plugin-ext/src/common/cache.ts create mode 100644 packages/plugin-ext/src/plugin/languages/inlay-hints.ts diff --git a/packages/plugin-ext/src/common/cache.ts b/packages/plugin-ext/src/common/cache.ts new file mode 100644 index 0000000000000..818302d38b53c --- /dev/null +++ b/packages/plugin-ext/src/common/cache.ts @@ -0,0 +1,51 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +// copied from https://github.com/microsoft/vscode/blob/53eac52308c4611000a171cc7bf1214293473c78/src/vs/workbench/api/common/cache.ts +export class Cache { + + private static readonly enableDebugLogging = false; + + private readonly _data = new Map(); + private _idPool = 1; + + constructor( + private readonly id: string + ) { } + + add(item: readonly T[]): number { + const id = this._idPool++; + this._data.set(id, item); + this.logDebugInfo(); + return id; + } + + get(pid: number, id: number): T | undefined { + return this._data.has(pid) ? this._data.get(pid)![id] : undefined; + } + + delete(id: number): void { + this._data.delete(id); + this.logDebugInfo(); + } + + private logDebugInfo(): void { + if (!Cache.enableDebugLogging) { + return; + } + console.log(`${this.id} cache size — ${this._data.size}`); + } +} diff --git a/packages/plugin-ext/src/common/plugin-api-rpc-model.ts b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts index 4f14694c89ad9..7b9447c15d1af 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc-model.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts @@ -360,6 +360,9 @@ export interface ReferenceContext { export type CacheId = number; export type ChainedCacheId = [CacheId, CacheId]; +export type CachedSessionItem = T & { cacheId?: ChainedCacheId }; +export type CachedSession = T & { cacheId?: CacheId }; + export interface DocumentLink { cacheId?: ChainedCacheId, range: Range; @@ -723,3 +726,32 @@ export interface CommentInfo { threads: CommentThread[]; commentingRanges: CommentingRanges; } + +export interface InlayHintLabelPart { + label: string; + tooltip?: string | MarkdownStringDTO; + location?: Location; + command?: Command; +} + +export interface InlayHint { + position: { lineNumber: number, column: number }; + label: string | InlayHintLabelPart[]; + tooltip?: string | MarkdownStringDTO | undefined; + kind?: InlayHintKind; + textEdits?: TextEdit[]; + paddingLeft?: boolean; + paddingRight?: boolean; +} + +export enum InlayHintKind { + Type = 1, + Parameter = 2, +} + +export interface InlayHintsProvider { + onDidChangeInlayHints?: TheiaEvent | undefined; + provideInlayHints(model: monaco.editor.ITextModel, range: Range, token: monaco.CancellationToken): InlayHint[] | undefined | Thenable; + resolveInlayHint?(hint: InlayHint, token: monaco.CancellationToken): InlayHint[] | undefined | Thenable; +} + diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index e16f64fb3125b..989cfc41d51a7 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -76,7 +76,10 @@ import { CommentThread, CommentThreadChangedEvent, CodeActionProviderDocumentation, - LinkedEditingRanges + LinkedEditingRanges, + InlayHint, + CachedSession, + CachedSessionItem } from './plugin-api-rpc-model'; import { ExtPluginApi } from './plugin-ext-api-contribution'; import { KeysToAnyValues, KeysToKeysToAnyValue } from './types'; @@ -1533,6 +1536,9 @@ export interface LanguagesExt { $provideSelectionRanges(handle: number, resource: UriComponents, positions: Position[], token: CancellationToken): PromiseLike; $provideDocumentColors(handle: number, resource: UriComponents, token: CancellationToken): PromiseLike; $provideColorPresentations(handle: number, resource: UriComponents, colorInfo: RawColorInfo, token: CancellationToken): PromiseLike; + $provideInlayHints(handle: number, resource: UriComponents, range: Range, token: CancellationToken): Promise; + $resolveInlayHint(handle: number, id: ChainedCacheId, token: CancellationToken): Promise; + $releaseInlayHints(handle: number, id: number): void; $provideRenameEdits(handle: number, resource: UriComponents, position: Position, newName: string, token: CancellationToken): PromiseLike; $resolveRenameLocation(handle: number, resource: UriComponents, position: Position, token: CancellationToken): PromiseLike; $provideDocumentSemanticTokens(handle: number, resource: UriComponents, previousResultId: number, token: CancellationToken): Promise; @@ -1588,6 +1594,8 @@ export interface LanguagesMain { $emitFoldingRangeEvent(handle: number, event?: any): void; $registerSelectionRangeProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void; $registerDocumentColorProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void; + $registerInlayHintsProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[], displayName?: string, eventHandle?: number): void; + $emitInlayHintsEvent(eventHandle: number, event?: any): void; $registerRenameProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[], supportsResolveInitialValues: boolean): void; $registerDocumentSemanticTokensProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[], legend: theia.SemanticTokensLegend, eventHandle: number | undefined): void; @@ -2001,3 +2009,6 @@ export interface SecretsMain { $setPassword(extensionId: string, key: string, value: string): Promise; $deletePassword(extensionId: string, key: string): Promise; } + +export type InlayHintDto = CachedSessionItem; +export type InlayHintsDto = CachedSession<{ hints: InlayHint[] }>; diff --git a/packages/plugin-ext/src/main/browser/languages-main.ts b/packages/plugin-ext/src/main/browser/languages-main.ts index ed08b07b5707f..53f07c26d172a 100644 --- a/packages/plugin-ext/src/main/browser/languages-main.ts +++ b/packages/plugin-ext/src/main/browser/languages-main.ts @@ -33,12 +33,13 @@ import { WorkspaceEditDto, WorkspaceTextEditDto, PluginInfo, - LanguageStatus as LanguageStatusDTO + LanguageStatus as LanguageStatusDTO, + InlayHintDto } from '../../common/plugin-api-rpc'; import { injectable, inject } from '@theia/core/shared/inversify'; import { SerializedDocumentFilter, MarkerData, Range, RelatedInformation, - MarkerSeverity, DocumentLink, WorkspaceSymbolParams, CodeAction, CompletionDto, CodeActionProviderDocumentation + MarkerSeverity, DocumentLink, WorkspaceSymbolParams, CodeAction, CompletionDto, CodeActionProviderDocumentation, InlayHint, InlayHintLabelPart } from '../../common/plugin-api-rpc-model'; import { RPCProtocol } from '../../common/rpc-protocol'; import { MonacoLanguages, WorkspaceSymbolProvider } from '@theia/monaco/lib/browser/monaco-languages'; @@ -806,6 +807,63 @@ export class LanguagesMainImpl implements LanguagesMain, Disposable { }, token); } + $registerInlayHintsProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[], displayName?: string, eventHandle?: number): void { + const languageSelector = this.toLanguageSelector(selector); + const inlayHintsProvider = this.createInlayHintsProvider(handle); + if (typeof eventHandle === 'number') { + const emitter = new Emitter(); + this.register(eventHandle, emitter); + inlayHintsProvider.onDidChangeInlayHints = emitter.event; + } + this.register(handle, (monaco.languages.registerInlayHintsProvider as RegistrationFunction)(languageSelector, inlayHintsProvider)); + + } + + createInlayHintsProvider(handle: number): monaco.languages.InlayHintsProvider { + return { + provideInlayHints: async (model: monaco.editor.ITextModel, range: Range, token: monaco.CancellationToken): Promise => { + const result = await this.proxy.$provideInlayHints(handle, model.uri, range, token); + if (!result) { + return; + } + return { + hints: result.hints.map(hint => reviveHint(hint)), + dispose: () => { + if (typeof result.cacheId === 'number') { + this.proxy.$releaseInlayHints(handle, result.cacheId); + } + } + }; + }, + resolveInlayHint: async (hint, token): Promise => { + const dto: InlayHintDto = hint; + if (typeof dto.cacheId !== 'number') { + return hint; + } + const result = await this.proxy.$resolveInlayHint(handle, dto.cacheId, token); + if (token.isCancellationRequested) { + return undefined; + } + if (!result) { + return hint; + } + return { + ...hint, + tooltip: result.tooltip, + label: reviveInlayLabel(result.label) + }; + }, + }; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + $emitInlayHintsEvent(eventHandle: number, event?: any): void { + const obj = this.services.get(eventHandle); + if (obj instanceof Emitter) { + obj.fire(event); + } + } + $registerQuickFixProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[], providedCodeActionKinds?: string[], documentation?: CodeActionProviderDocumentation): void { @@ -1165,6 +1223,31 @@ function reviveOnEnterRules(onEnterRules?: SerializedOnEnterRule[]): monaco.lang return onEnterRules.map(reviveOnEnterRule); } +function reviveInlayLabel(label: string | InlayHintLabelPart[]): string | monaco.languages.InlayHintLabelPart[] { + let monacoLabel: string | monaco.languages.InlayHintLabelPart[]; + if (typeof label === 'string') { + monacoLabel = label; + } else { + const parts: monaco.languages.InlayHintLabelPart[] = []; + for (const part of label) { + const result: monaco.languages.InlayHintLabelPart = { + ...part, + location: !!part.location ? { range: part.location?.range, uri: monaco.Uri.revive(part.location.uri) } : undefined + }; + parts.push(result); + } + monacoLabel = parts; + } + return monacoLabel; +} + +function reviveHint(hint: InlayHint): monaco.languages.InlayHint { + return { + ...hint, + label: reviveInlayLabel(hint.label) + }; +} + function toMonacoAction(action: CodeAction): monaco.languages.CodeAction { return { ...action, diff --git a/packages/plugin-ext/src/plugin/custom-editors.ts b/packages/plugin-ext/src/plugin/custom-editors.ts index c32ade3b6e15a..2bce09b56f322 100644 --- a/packages/plugin-ext/src/plugin/custom-editors.ts +++ b/packages/plugin-ext/src/plugin/custom-editors.ts @@ -29,6 +29,7 @@ import { WebviewImpl, WebviewsExtImpl } from './webviews'; import { CancellationToken, CancellationTokenSource } from '@theia/core/lib/common/cancellation'; import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { WorkspaceExtImpl } from './workspace'; +import { Cache } from '../common/cache'; export class CustomEditorsExtImpl implements CustomEditorsExt { private readonly proxy: CustomEditorsMain; @@ -333,36 +334,3 @@ class CustomDocumentStore { } } -// copied from https://github.com/microsoft/vscode/blob/53eac52308c4611000a171cc7bf1214293473c78/src/vs/workbench/api/common/cache.ts -class Cache { - private static readonly enableDebugLogging = false; - private readonly _data = new Map(); - private _idPool = 1; - - constructor( - private readonly id: string - ) { } - - add(item: readonly T[]): number { - const id = this._idPool++; - this._data.set(id, item); - this.logDebugInfo(); - return id; - } - - get(pid: number, id: number): T | undefined { - return this._data.has(pid) ? this._data.get(pid)![id] : undefined; - } - - delete(id: number): void { - this._data.delete(id); - this.logDebugInfo(); - } - - private logDebugInfo(): void { - if (!Cache.enableDebugLogging) { - return; - } - console.log(`${this.id} cache size — ${this._data.size}`); - } -} diff --git a/packages/plugin-ext/src/plugin/languages.ts b/packages/plugin-ext/src/plugin/languages.ts index 860671c56457d..0e5f5caf8b319 100644 --- a/packages/plugin-ext/src/plugin/languages.ts +++ b/packages/plugin-ext/src/plugin/languages.ts @@ -25,6 +25,8 @@ import { WorkspaceEditDto, PluginInfo, Plugin, + InlayHintsDto, + InlayHintDto, } from '../common/plugin-api-rpc'; import { RPCProtocol } from '../common/rpc-protocol'; import * as theia from '@theia/plugin'; @@ -101,6 +103,7 @@ import { DisposableCollection, disposableTimeout, Disposable as TheiaDisposable import { Severity } from '@theia/core/lib/common/severity'; import { LinkedEditingRangeAdapter } from './languages/linked-editing-range'; import { serializeEnterRules, serializeIndentation, serializeRegExp } from './languages-utils'; +import { InlayHintsAdapter } from './languages/inlay-hints'; type Adapter = CompletionAdapter | SignatureHelpAdapter | @@ -124,6 +127,7 @@ type Adapter = CompletionAdapter | FoldingProviderAdapter | SelectionRangeProviderAdapter | ColorProviderAdapter | + InlayHintsAdapter | RenameAdapter | CallHierarchyAdapter | DocumentRangeSemanticTokensAdapter | @@ -602,6 +606,35 @@ export class LanguagesExtImpl implements LanguagesExt { } // ### Color Provider end + // ### InlayHints Provider begin + registerInlayHintsProvider(selector: theia.DocumentSelector, provider: theia.InlayHintsProvider, pluginInfo: PluginInfo): theia.Disposable { + const eventHandle = typeof provider.onDidChangeInlayHints === 'function' ? this.nextCallId() : undefined; + const callId = this.addNewAdapter(new InlayHintsAdapter(provider, this.documents, this.commands)); + this.proxy.$registerInlayHintsProvider(callId, pluginInfo, this.transformDocumentSelector(selector)); + + let result = this.createDisposable(callId); + + if (eventHandle !== undefined) { + const subscription = provider.onDidChangeInlayHints!(() => this.proxy.$emitInlayHintsEvent(eventHandle)); + result = Disposable.from(result, subscription); + } + + return result; + } + + $provideInlayHints(handle: number, resource: UriComponents, range: Range, token: theia.CancellationToken): Promise { + return this.withAdapter(handle, InlayHintsAdapter, adapter => adapter.provideInlayHints(URI.revive(resource), range, token), undefined); + } + + $resolveInlayHint(handle: number, id: ChainedCacheId, token: theia.CancellationToken): Promise { + return this.withAdapter(handle, InlayHintsAdapter, adapter => adapter.resolveInlayHint(id, token), undefined); + } + + $releaseInlayHints(handle: number, id: number): void { + this.withAdapter(handle, InlayHintsAdapter, async adapter => adapter.releaseHints(id), undefined); + } + // ### InlayHints Provider end + // ### Folding Range Provider begin registerFoldingRangeProvider(selector: theia.DocumentSelector, provider: theia.FoldingRangeProvider, pluginInfo: PluginInfo): theia.Disposable { const callId = this.addNewAdapter(new FoldingProviderAdapter(provider, this.documents)); diff --git a/packages/plugin-ext/src/plugin/languages/inlay-hints.ts b/packages/plugin-ext/src/plugin/languages/inlay-hints.ts new file mode 100644 index 0000000000000..3a1562bd139a6 --- /dev/null +++ b/packages/plugin-ext/src/plugin/languages/inlay-hints.ts @@ -0,0 +1,149 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// copied and modified from https://github.com/microsoft/vscode/blob/1.65.0/src/vs/workbench/api/common/extHostLanguageFeatures.ts#L1178-L1288 + +import * as theia from '@theia/plugin'; +import * as Converter from '../type-converters'; +import { Cache } from '../../common/cache'; +import { ChainedCacheId, InlayHint, InlayHintLabelPart, Range } from '../../common/plugin-api-rpc-model'; +import { CommandRegistryImpl } from '../command-registry'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { DocumentsExtImpl } from '../documents'; +import { InlayHintDto, InlayHintsDto } from '../../common'; +import { URI } from '@theia/core/shared/vscode-uri'; +import { isLocationArray } from './util'; + +export class InlayHintsAdapter { + + private cache = new Cache('InlayHints'); + private readonly disposables = new Map(); + + constructor( + private readonly provider: theia.InlayHintsProvider, + private readonly documents: DocumentsExtImpl, + private readonly commands: CommandRegistryImpl + ) { } + + async provideInlayHints(resource: URI, range: Range, token: theia.CancellationToken): Promise { + const documentData = this.documents.getDocumentData(resource); + if (!documentData) { + return Promise.reject(new Error(`There are no documents for ${resource}`)); + } + + const doc = documentData.document; + const ran = Converter.toRange(range); + const hints = await this.provider.provideInlayHints(doc, ran, token); + + if (!Array.isArray(hints) || hints.length === 0) { + return undefined; + } + + if (token.isCancellationRequested) { + return undefined; + } + + const pid = this.cache.add(hints); + this.disposables.set(pid, new DisposableCollection()); + const result: InlayHintsDto = { hints: [], cacheId: pid }; + + for (let i = 0; i < hints.length; i++) { + if (this.isValidInlayHint(hints[i], ran)) { + result.hints.push(this.convertInlayHint(hints[i], [pid, i])); + } + } + + return result; + } + + async resolveInlayHint(id: ChainedCacheId, token: theia.CancellationToken): Promise { + if (typeof this.provider.resolveInlayHint !== 'function') { + return undefined; + } + const item = this.cache.get(...id); + if (!item) { + return undefined; + } + const hint = await this.provider.resolveInlayHint!(item, token); + if (!hint) { + return undefined; + } + if (!this.isValidInlayHint(hint)) { + return undefined; + } + return this.convertInlayHint(hint, id); + } + + private isValidInlayHint(hint: theia.InlayHint, range?: theia.Range): boolean { + if (hint.label.length === 0 || Array.isArray(hint.label) && hint.label.every(part => part.value.length === 0)) { + return false; + } + if (range && !range.contains(hint.position)) { + return false; + } + return true; + } + + private convertInlayHint(hint: theia.InlayHint, id: ChainedCacheId): InlayHintDto { + + const disposables = this.disposables.get(id[0]); + if (!disposables) { + throw Error('DisposableCollection is missing...'); + } + + const result: InlayHintDto = { + label: '', // fill-in below. + cacheId: id, + tooltip: hint.tooltip, + position: Converter.fromPosition(hint.position), + textEdits: hint.textEdits && hint.textEdits.map(Converter.fromTextEdit), + kind: hint.kind && Converter.InlayHintKind.from(hint.kind), + paddingLeft: hint.paddingLeft, + paddingRight: hint.paddingRight, + }; + + if (typeof hint.label === 'string') { + result.label = hint.label; + } else { + result.label = hint.label.map(part => { + const partResult: InlayHintLabelPart = { label: part.value }; + if (part.tooltip) { + partResult.tooltip = typeof partResult === 'string' ? part.tooltip : Converter.fromMarkdown(part.tooltip); + } + if (isLocationArray(part.location)) { + partResult.location = Converter.fromLocation(part.location); + } + if (part.command) { + partResult.command = this.commands.converter.toSafeCommand(part.command, disposables); + } + return partResult; + }); + } + + return result; + } + + async releaseHints(id: number): Promise { + this.disposables.get(id)?.dispose(); + this.disposables.delete(id); + this.cache.delete(id); + } + +} diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 2538c6089a851..fc666bdde0467 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -149,7 +149,10 @@ import { LinkedEditingRanges, LanguageStatusSeverity, TextDocumentChangeReason, - InputBoxValidationSeverity + InputBoxValidationSeverity, + InlayHint, + InlayHintKind, + InlayHintLabelPart } from './types-impl'; import { AuthenticationExtImpl } from './authentication-ext'; import { SymbolKind } from '../common/plugin-api-rpc-model'; @@ -747,6 +750,9 @@ export function createAPIFactory( registerColorProvider(selector: theia.DocumentSelector, provider: theia.DocumentColorProvider): theia.Disposable { return languagesExt.registerColorProvider(selector, provider, pluginToPluginInfo(plugin)); }, + registerInlayHintsProvider(selector: theia.DocumentSelector, provider: theia.InlayHintsProvider): theia.Disposable { + return languagesExt.registerInlayHintsProvider(selector, provider, pluginToPluginInfo(plugin)); + }, registerFoldingRangeProvider(selector: theia.DocumentSelector, provider: theia.FoldingRangeProvider): theia.Disposable { return languagesExt.registerFoldingRangeProvider(selector, provider, pluginToPluginInfo(plugin)); }, @@ -1039,7 +1045,10 @@ export function createAPIFactory( CancellationError, ExtensionMode, LinkedEditingRanges, - InputBoxValidationSeverity + InputBoxValidationSeverity, + InlayHint, + InlayHintKind, + InlayHintLabelPart }; }; } diff --git a/packages/plugin-ext/src/plugin/type-converters.ts b/packages/plugin-ext/src/plugin/type-converters.ts index 4b80b2b11a2f9..19c5087f11a55 100644 --- a/packages/plugin-ext/src/plugin/type-converters.ts +++ b/packages/plugin-ext/src/plugin/type-converters.ts @@ -129,7 +129,7 @@ export function fromRange(range: theia.Range | undefined): model.Range | undefin }; } -export function fromPosition(position: types.Position): Position { +export function fromPosition(position: types.Position | theia.Position): Position { return { lineNumber: position.line + 1, column: position.character + 1 }; } @@ -1271,3 +1271,11 @@ export function pluginToPluginInfo(plugin: Plugin): rpc.PluginInfo { }; } +export namespace InlayHintKind { + export function from(kind: theia.InlayHintKind): model.InlayHintKind { + return kind; + } + export function to(kind: model.InlayHintKind): theia.InlayHintKind { + return kind; + } +} diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index ca4ab2c4cc13b..422e4c9565033 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -2465,6 +2465,40 @@ export enum ColorFormat { HSL = 2 } +@es5ClassCompat +export class InlayHintLabelPart implements theia.InlayHintLabelPart { + value: string; + tooltip?: string | theia.MarkdownString | undefined; + location?: Location | undefined; + command?: theia.Command | undefined; + + constructor(value: string) { + this.value = value; + } +} + +@es5ClassCompat +export class InlayHint implements theia.InlayHint { + position: theia.Position; + label: string | InlayHintLabelPart[]; + tooltip?: string | theia.MarkdownString | undefined; + kind?: InlayHintKind; + textEdits?: TextEdit[]; + paddingLeft?: boolean; + paddingRight?: boolean; + + constructor(position: theia.Position, label: string | InlayHintLabelPart[], kind?: InlayHintKind) { + this.position = position; + this.label = label; + this.kind = kind; + } +} + +export enum InlayHintKind { + Type = 1, + Parameter = 2, +} + @es5ClassCompat export class FoldingRange { start: number; diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index bd47900aff704..6d4aef73f0826 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -7600,6 +7600,176 @@ export module '@theia/plugin' { provideColorPresentations(color: Color, context: { document: TextDocument, range: Range }, token: CancellationToken): ProviderResult; } + /** + * Inlay hint kinds. + * + * The kind of an inline hint defines its appearance, e.g the corresponding foreground and background colors are being + * used. + */ + export enum InlayHintKind { + /** + * An inlay hint that for a type annotation. + */ + Type = 1, + /** + * An inlay hint that is for a parameter. + */ + Parameter = 2, + } + + /** + * An inlay hint label part allows for interactive and composite labels of inlay hints. + */ + export class InlayHintLabelPart { + + /** + * The value of this label part. + */ + value: string; + + /** + * The tooltip text when you hover over this label part. + * + * *Note* that this property can be set late during + * {@link InlayHintsProvider.resolveInlayHint resolving} of inlay hints. + */ + tooltip?: string | MarkdownString | undefined; + + /** + * An optional {@link Location source code location} that represents this label + * part. + * + * The editor will use this location for the hover and for code navigation features: This + * part will become a clickable link that resolves to the definition of the symbol at the + * given location (not necessarily the location itself), it shows the hover that shows at + * the given location, and it shows a context menu with further code navigation commands. + * + * *Note* that this property can be set late during + * {@link InlayHintsProvider.resolveInlayHint resolving} of inlay hints. + */ + location?: Location | undefined; + + /** + * An optional command for this label part. + * + * The editor renders parts with commands as clickable links. The command is added to the context menu + * when a label part defines {@link InlayHintLabelPart.location location} and {@link InlayHintLabelPart.command command} . + * + * *Note* that this property can be set late during + * {@link InlayHintsProvider.resolveInlayHint resolving} of inlay hints. + */ + command?: Command | undefined; + + /** + * Creates a new inlay hint label part. + * + * @param value The value of the part. + */ + constructor(value: string); + } + + /** + * Inlay hint information. + */ + export class InlayHint { + + /** + * The position of this hint. + */ + position: Position; + + /** + * The label of this hint. A human readable string or an array of {@link InlayHintLabelPart label parts}. + * + * *Note* that neither the string nor the label part can be empty. + */ + label: string | InlayHintLabelPart[]; + + /** + * The tooltip text when you hover over this item. + * + * *Note* that this property can be set late during + * {@link InlayHintsProvider.resolveInlayHint resolving} of inlay hints. + */ + tooltip?: string | MarkdownString | undefined; + + /** + * The kind of this hint. The inlay hint kind defines the appearance of this inlay hint. + */ + kind?: InlayHintKind; + + /** + * Optional {@link TextEdit text edits} that are performed when accepting this inlay hint. The default + * gesture for accepting an inlay hint is the double click. + * + * *Note* that edits are expected to change the document so that the inlay hint (or its nearest variant) is + * now part of the document and the inlay hint itself is now obsolete. + * + * *Note* that this property can be set late during + * {@link InlayHintsProvider.resolveInlayHint resolving} of inlay hints. + */ + textEdits?: TextEdit[]; + + /** + * Render padding before the hint. Padding will use the editor's background color, + * not the background color of the hint itself. That means padding can be used to visually + * align/separate an inlay hint. + */ + paddingLeft?: boolean; + + /** + * Render padding after the hint. Padding will use the editor's background color, + * not the background color of the hint itself. That means padding can be used to visually + * align/separate an inlay hint. + */ + paddingRight?: boolean; + + /** + * Creates a new inlay hint. + * + * @param position The position of the hint. + * @param label The label of the hint. + * @param kind The {@link InlayHintKind kind} of the hint. + */ + constructor(position: Position, label: string | InlayHintLabelPart[], kind?: InlayHintKind); + } + + /** + * The inlay hints provider interface defines the contract between extensions and + * the inlay hints feature. + */ + export interface InlayHintsProvider { + + /** + * An optional event to signal that inlay hints from this provider have changed. + */ + onDidChangeInlayHints?: Event; + + /** + * Provide inlay hints for the given range and document. + * + * *Note* that inlay hints that are not {@link Range.contains contained} by the given range are ignored. + * + * @param document The document in which the command was invoked. + * @param range The range for which inlay hints should be computed. + * @param token A cancellation token. + * @return An array of inlay hints or a thenable that resolves to such. + */ + provideInlayHints(document: TextDocument, range: Range, token: CancellationToken): ProviderResult; + + /** + * Given an inlay hint fill in {@link InlayHint.tooltip tooltip}, {@link InlayHint.textEdits text edits}, + * or complete label {@link InlayHintLabelPart parts}. + * + * *Note* that the editor will resolve an inlay hint at most once. + * + * @param hint An inlay hint. + * @param token A cancellation token. + * @return The resolved inlay hint or a thenable that resolves to such. It is OK to return the given `item`. When no result is returned, the given `item` will be used. + */ + resolveInlayHint?(hint: T, token: CancellationToken): ProviderResult; + } + /** * A line based folding range. To be valid, start and end line must a zero or larger and smaller than the number of lines in the document. * Invalid ranges will be ignored. @@ -9677,6 +9847,19 @@ export module '@theia/plugin' { */ export function registerColorProvider(selector: DocumentSelector, provider: DocumentColorProvider): Disposable; + /** + * Register a inlay hints provider. + * + * Multiple providers can be registered for a language. In that case providers are asked in + * parallel and the results are merged. A failing provider (rejected promise or exception) will + * not cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider An inlay hints provider. + * @return A {@link Disposable} that unregisters this provider when being disposed. + */ + export function registerInlayHintsProvider(selector: DocumentSelector, provider: InlayHintsProvider): Disposable; + /** * Register a folding range provider. *