From 2d8e6da59f7e106701aa7e4a0a57a1af263ad08d Mon Sep 17 00:00:00 2001 From: Sam McCall Date: Fri, 28 May 2021 13:28:30 +0200 Subject: [PATCH 1/2] Add proposed textDocument/inlayHints to protocol & client. This addresses the FR https://github.com/microsoft/language-server-protocol/issues/956 And corresponds to the spec in https://github.com/microsoft/language-server-protocol/pull/1249 --- client/src/common/commonClient.ts | 5 +- client/src/common/proposed.inlayHints.ts | 92 ++++++++++++++++ client/typings/vscode-proposed.d.ts | 81 ++++++++++++++ protocol/src/common/api.ts | 15 +++ protocol/src/common/proposed.inlayHints.ts | 116 +++++++++++++++++++++ 5 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 client/src/common/proposed.inlayHints.ts create mode 100644 protocol/src/common/proposed.inlayHints.ts diff --git a/client/src/common/commonClient.ts b/client/src/common/commonClient.ts index 0987df856..d06f30f57 100644 --- a/client/src/common/commonClient.ts +++ b/client/src/common/commonClient.ts @@ -55,10 +55,13 @@ export abstract class CommonLanguageClient extends BaseLanguageClient { // Exporting proposed protocol. import { DiagnosticFeature } from './proposed.diagnostic'; +import { InlayHintsFeature } from './proposed.inlayHints'; + export namespace ProposedFeatures { export function createAll(_client: BaseLanguageClient): (StaticFeature | DynamicFeature)[] { let result: (StaticFeature | DynamicFeature)[] = [ - new DiagnosticFeature(_client) + new DiagnosticFeature(_client), + new InlayHintsFeature(_client), ]; return result; } diff --git a/client/src/common/proposed.inlayHints.ts b/client/src/common/proposed.inlayHints.ts new file mode 100644 index 000000000..78a30f039 --- /dev/null +++ b/client/src/common/proposed.inlayHints.ts @@ -0,0 +1,92 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import { languages as Languages, Disposable, TextDocument, Range, ProviderResult, InlayHintKind, InlayHint as VInlayHint, InlayHintsProvider } from 'vscode'; + +import { + ClientCapabilities, CancellationToken, ServerCapabilities, DocumentSelector, Proposed +} from 'vscode-languageserver-protocol'; + +import { TextDocumentFeature, BaseLanguageClient, Middleware } from './client'; +import * as p2c from './protocolConverter'; + +function ensure(target: T, key: K): T[K] { + if (target[key] === void 0) { + target[key] = {} as any; + } + return target[key]; +} + +export interface ProvideInlayHintsSignature { + (this: void, document: TextDocument, range: Range, token: CancellationToken): ProviderResult; +} + +export interface InlayHintsMiddleware { + provideInlayHints?: (this: void, document: TextDocument, range: Range, token: CancellationToken, next: ProvideInlayHintsSignature) => ProviderResult; +} + +namespace protocol2code { + function asInlayHintKind(_: p2c.Converter, value: string | null | undefined) : InlayHintKind | undefined { + switch (value) { + case Proposed.InlayHintCategory.Parameter: + return InlayHintKind.Parameter; + case Proposed.InlayHintCategory.Type: + return InlayHintKind.Type; + default: + return InlayHintKind.Other; + } + } + export function asInlayHint(converter: p2c.Converter, item: Proposed.InlayHint) : VInlayHint { + const result = new VInlayHint(item.label.trim(), converter.asPosition(item.position), asInlayHintKind(converter, item.category)); + result.whitespaceBefore = item.label.startsWith(' '); + result.whitespaceAfter = item.label.endsWith(' '); + return result; + } +} + +export class InlayHintsFeature extends TextDocumentFeature { + + constructor(client: BaseLanguageClient) { + super(client, Proposed.InlayHintsRequest.type); + } + + public fillClientCapabilities(capabilities: ClientCapabilities & Proposed.$InlayHintsClientCapabilities): void { + let capability = ensure(ensure(capabilities, 'textDocument')!, 'inlayHints')!; + capability.dynamicRegistration = true; + } + + public initialize(capabilities: ServerCapabilities & Proposed.$InlayHintsServerCapabilities, documentSelector: DocumentSelector): void { + let [id, options] = this.getRegistration(documentSelector, capabilities.inlayHintsProvider); + if (!id || !options) { + return; + } + this.register({ id: id, registerOptions: options }); + } + + protected registerLanguageProvider(options: Proposed.InlayHintsRegistrationOptions): [Disposable, InlayHintsProvider] { + const provider: InlayHintsProvider = { + provideInlayHints: (document, range, token) => { + const client = this._client; + const provideInlayHints: ProvideInlayHintsSignature = (document, range, token) => { + const requestParams: Proposed.InlayHintsParams = { + textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), + range: client.code2ProtocolConverter.asRange(range) + }; + return client.sendRequest(Proposed.InlayHintsRequest.type, requestParams, token).then( + (m: Proposed.InlayHint[]) => m.map(h => protocol2code.asInlayHint(client.protocol2CodeConverter, h)), + (error: any) => { + return client.handleFailedRequest(Proposed.InlayHintsRequest.type, token, error, null); + } + ); + }; + const middleware = client.clientOptions.middleware as (Middleware & InlayHintsMiddleware) | undefined; + return middleware?.provideInlayHints + ? middleware.provideInlayHints(document, range, token, provideInlayHints) + : provideInlayHints(document, range, token); + } + }; + return [Languages.registerInlayHintsProvider(options.documentSelector!, provider), provider]; + } +} \ No newline at end of file diff --git a/client/typings/vscode-proposed.d.ts b/client/typings/vscode-proposed.d.ts index 9a03b0fcd..0822813d4 100644 --- a/client/typings/vscode-proposed.d.ts +++ b/client/typings/vscode-proposed.d.ts @@ -45,4 +45,85 @@ declare module 'vscode' { // todo@API proper event type export const onDidChangeOpenEditors: Event; } + + //#region https://github.com/microsoft/vscode/issues/16221 + + // todo@API Split between Inlay- and OverlayHints (InlayHint are for a position, OverlayHints for a non-empty range) + // todo@API add "mini-markdown" for links and styles + // (done) remove description + // (done) rename to InlayHint + // (done) add InlayHintKind with type, argument, etc + + export namespace languages { + /** + * 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; + } + + export enum InlayHintKind { + Other = 0, + Type = 1, + Parameter = 2, + } + + /** + * Inlay hint information. + */ + export class InlayHint { + /** + * The text of the hint. + */ + text: string; + /** + * The position of this hint. + */ + position: Position; + /** + * The kind of this hint. + */ + kind?: InlayHintKind; + /** + * Whitespace before the hint. + */ + whitespaceBefore?: boolean; + /** + * Whitespace after the hint. + */ + whitespaceAfter?: boolean; + + // todo@API make range first argument + constructor(text: string, position: Position, 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 have changed. + * @see {@link EventEmitter} + */ + onDidChangeInlayHints?: Event; + + /** + * + * @param model 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 A list of inlay hints or a thenable that resolves to such. + */ + provideInlayHints(model: TextDocument, range: Range, token: CancellationToken): ProviderResult; + } + //#endregion } \ No newline at end of file diff --git a/protocol/src/common/api.ts b/protocol/src/common/api.ts index f5e4b67ec..dcaa940c4 100644 --- a/protocol/src/common/api.ts +++ b/protocol/src/common/api.ts @@ -88,4 +88,19 @@ export namespace Proposed { export type WorkspaceDiagnosticReport = diag.WorkspaceDiagnosticReport; export type WorkspaceDiagnosticReportPartialResult = diag.WorkspaceDiagnosticReportPartialResult; export const DiagnosticRefreshRequest: typeof diag.DiagnosticRefreshRequest = diag.DiagnosticRefreshRequest; +} + +import * as inlay from './proposed.inlayHints'; + +export namespace Proposed { + export type InlayHintCategory = inlay.InlayHintCategory; + export const InlayHintCategory = inlay.InlayHintCategory; + export type InlayHint = inlay.InlayHint; + export type InlayHintsClientCapabilities = inlay.InlayHintsClientCapabilities; + export type InlayHintsOptions = inlay.InlayHintsOptions; + export type InlayHintsRegistrationOptions = inlay.InlayHintsRegistrationOptions; + export type $InlayHintsClientCapabilities = inlay.$InlayHintsClientCapabilities; + export type $InlayHintsServerCapabilities = inlay.$InlayHintsServerCapabilities; + export type InlayHintsParams = inlay.InlayHintsParams; + export const InlayHintsRequest: typeof inlay.InlayHintsRequest = inlay.InlayHintsRequest; } \ No newline at end of file diff --git a/protocol/src/common/proposed.inlayHints.ts b/protocol/src/common/proposed.inlayHints.ts new file mode 100644 index 000000000..bbfcddc18 --- /dev/null +++ b/protocol/src/common/proposed.inlayHints.ts @@ -0,0 +1,116 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import { ProtocolRequestType } from './messages'; +import { Position, Range, TextDocumentIdentifier } from 'vscode-languageserver-types'; +import { + WorkDoneProgressOptions, WorkDoneProgressParams, PartialResultParams, TextDocumentRegistrationOptions, TextDocumentClientCapabilities +} from './protocol'; + +/** + * Well-known kinds of information conveyed by InlayHints. + * Clients may choose which categories to display according to user preferences. + * + * @since 3.17.0 + */ + export enum InlayHintCategory { + /** + * The range is an expression passed as an argument to a function. + * The label is the name of the parameter. + */ + Parameter = 'parameter', + /** + * The range is an entity whose type is unknown. + * The label is its inferred type. + */ + Type = 'type' +} + +/** + * An inlay hint is a short textual annotation for a range of source code. + * + * @since 3.17.0 + */ + export interface InlayHint { + /** + * The text to be shown. + */ + label: string; + + /** + * The position within the code this hint is attached to. + */ + position: Position; + + /** + * The kind of information this hint conveys. + * May be an InlayHintCategory or any other value, clients should treat + * unrecognized values as if missing. + */ + category?: string; +} + + +/** + * Client capabilities specific to the inlayHints request. + * + * @since 3.17.0 + */ +export interface InlayHintsClientCapabilities { + /** + * Whether implementation supports dynamic registration. If this is set to + * `true` the client supports the new `(TextDocumentRegistrationOptions & + * StaticRegistrationOptions)` return value for the corresponding server + * capability as well. + */ + dynamicRegistration?: boolean; +} + +export interface $InlayHintsClientCapabilities { + textDocument?: TextDocumentClientCapabilities & { + inlayHints?: InlayHintsClientCapabilities; + } +} + +export interface InlayHintsServerCapabilities { +} + +export interface InlayHintsOptions extends WorkDoneProgressOptions { +} + +export interface InlayHintsRegistrationOptions extends TextDocumentRegistrationOptions, InlayHintsOptions { +} + +export interface $InlayHintsServerCapabilities { + inlayHintsProvider?: InlayHintsOptions; +} + +export interface InlayHintsParams extends WorkDoneProgressParams, PartialResultParams { + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + + /** + * The range the inlay hints are requested for. + * If unset, returns all hints for the document. + */ + range?: Range; + + /** + * The categories of inlay hints that are interesting to the client. + * The client should filter out hints of other categories, so the server may + * skip computing them. + */ + only?: string[]; +} + +/** + * The `textDocument/inlayHints` request is sent from the client to the server to retrieve inlay hints for a document. + */ +export namespace InlayHintsRequest { + export const method: 'textDocument/inlayHints' = 'textDocument/inlayHints'; + export const type = new ProtocolRequestType(method); +} \ No newline at end of file From fd641e86bab07341a86c58b37d241357c90eadee Mon Sep 17 00:00:00 2001 From: Sam McCall Date: Fri, 4 Jun 2021 11:12:19 +0200 Subject: [PATCH 2/2] Add proposed protocol spec under protocol/. This obsoletes https://github.com/microsoft/language-server-protocol/pull/1249 --- protocol/src/common/proposed.inlayHints.md | 123 +++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 protocol/src/common/proposed.inlayHints.md diff --git a/protocol/src/common/proposed.inlayHints.md b/protocol/src/common/proposed.inlayHints.md new file mode 100644 index 000000000..94aa1a91c --- /dev/null +++ b/protocol/src/common/proposed.inlayHints.md @@ -0,0 +1,123 @@ +#### Inlay hints + +Inlay hints are short textual annotations that are attached to points in the source code. +These typically spell out some inferred information, such as the parameter name when passing a value to a function. + +```typescript +/** + * Well-known kinds of information conveyed by InlayHints. + * Clients may choose which categories to display according to user preferences. + */ +export enum InlayHintCategory { + /** + * The range is an expression passed as an argument to a function. + * The label is the name of the parameter. + */ + Parameter = 'parameter', + /** + * The range is an entity whose type is unknown. + * The label is its inferred type. + */ + Type = 'type' +} +``` + +The `textDocument/inlayHints` request is sent from the client to the server to retrieve inlay hints for a document. + +_Client Capabilities_: + +* property name (optional): `textDocument.inlayHints` +* property type: `InlayHintsClientCapabilities` defined as follows: + +```typescript +interface InlayHintsClientCapabilities { + /** + * Whether implementation supports dynamic registration. If this is set to + * `true` the client supports the new `(TextDocumentRegistrationOptions & + * StaticRegistrationOptions)` return value for the corresponding server + * capability as well. + */ + dynamicRegistration?: boolean; +} +``` + +_Server Capability_: + +* property name (optional): `inlayHintsProvider` +* property type: `boolean | InlayHintsOptions | InlayHintsRegistrationOptions` is defined as follows: + +```typescript +export interface InlayHintsOptions extends WorkDoneProgressOptions { +} +``` + +_Registration Options_: `InlayHintsRegistrationOptions` defined as follows: + +```typescript +export interface InlayHintsRegistrationOptions extends + TextDocumentRegistrationOptions, InlayHintsOptions { +} +``` + +_Request_: + +* method: `textDocument/inlayHints` +* params: `InlayHintsParams` defined as follows: + +```typescript +export interface InlayHintsParams extends WorkDoneProgressParams, PartialResultParams { + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + + /** + * The range the inlay hints are requested for. + * If unset, returns all hints for the document. + */ + range?: Range; + + /** + * The categories of inlay hints that are interesting to the client. + * The client should filter out hints of other categories, so the server may + * skip computing them. + */ + only?: string[]; +} +``` + +_Response_: + +* result: `InlayHint[]` +* partial result: `InlayHint[]` +* error: code and message set in case an exception happens during the 'textDocument/inlayHint' request + +`InlayHint` is defined as follows: + +```typescript +/** + * An inlay hint is a short textual annotation for a range of source code. + */ +export interface InlayHint { + /** + * The text to be shown. + */ + label: string; + + /** + * The position within the code this hint is attached to. + */ + position: Position; + + /** + * The kind of information this hint conveys. + * May be an InlayHintCategory or any other value, clients should treat + * unrecognized values as if missing. + */ + category?: string; +} +``` + +**TODO**: Do we need a `/refresh` server->client call, like with SemanticTokens and Code Lens? + +**TODO**: Likely future evolution: add a `/resolve` call enabling interactions with hints. Should we foreshadow this?