From ad3ed988d1503954ed4370f898710118e8cff821 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 20 Aug 2022 14:29:03 +0200 Subject: [PATCH 1/4] feat: support `textDocument/inlayHint` request from 3.17.0 spec --- README.md | 5 +- src/features/inlay-hints.ts | 98 +++++++++++++++++++++++++++++++++++++ src/lsp-connection.ts | 3 +- src/lsp-server.spec.ts | 26 +++++++++- src/lsp-server.ts | 9 +++- 5 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 src/features/inlay-hints.ts diff --git a/README.md b/README.md index 2fe29754..d30d8c47 100644 --- a/README.md +++ b/README.md @@ -379,6 +379,8 @@ Most of the time, you'll execute commands with arguments retrieved from another ## Inlay hints (`typescript/inlayHints`) (experimental) +> !!! This implementation is deprecated. Use the spec-compliant `textDocument/inlayHint` request instead. !!! + Supports experimental inline hints. ```ts @@ -511,6 +513,7 @@ interface DefinitionSymbol { - [x] textDocument/documentHighlight - [x] textDocument/documentSymbol - [x] textDocument/executeCommand +- [x] textDocument/inlayHint (no support for `inlayHint/resolve` or `workspace/inlayHint/refresh`) - [x] textDocument/formatting - [x] textDocument/rangeFormatting - [x] textDocument/hover @@ -518,7 +521,7 @@ interface DefinitionSymbol { - [x] textDocument/references - [x] textDocument/signatureHelp - [x] textDocument/calls (experimental) -- [x] typescript/inlayHints (experimental, supported from Typescript v4.4.2) +- [x] typescript/inlayHints (experimental, supported from Typescript v4.4.2) DEPRECATED (use `textDocument/inlayHint` instead) - [x] workspace/symbol - [x] workspace/didChangeConfiguration - [x] workspace/executeCommand diff --git a/src/features/inlay-hints.ts b/src/features/inlay-hints.ts new file mode 100644 index 00000000..468d4ee7 --- /dev/null +++ b/src/features/inlay-hints.ts @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 TypeFox and others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + */ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import tsp from 'typescript/lib/protocol.d.js'; +import * as lsp from 'vscode-languageserver'; +import API from '../utils/api.js'; +import type { ConfigurationManager } from '../configuration-manager.js'; +import type { LspDocuments } from '../document.js'; +import type { TspClient } from '../tsp-client.js'; +import type { LspClient } from '../lsp-client.js'; +import { CommandTypes } from '../tsp-command-types.js'; +import { Position } from '../utils/typeConverters.js'; +import { uriToPath } from '../protocol-translation.js'; + +export class TypeScriptInlayHintsProvider { + public static readonly minVersion = API.v440; + + public static async provideInlayHints( + uri: lsp.DocumentUri, + range: lsp.Range, + documents: LspDocuments, + tspClient: TspClient, + lspClient: LspClient, + configurationManager: ConfigurationManager + ): Promise { + if (tspClient.apiVersion.lt(TypeScriptInlayHintsProvider.minVersion)) { + lspClient.showErrorMessage('Inlay Hints request failed. Requires TypeScript 4.4+.'); + return []; + } + + const file = uriToPath(uri); + + if (!file) { + lspClient.showErrorMessage('Inlay Hints request failed. No resource provided.'); + return []; + } + + const document = documents.get(file); + + if (!document) { + lspClient.showErrorMessage('Inlay Hints request failed. File not opened in the editor.'); + return []; + } + + if (!areInlayHintsEnabledForFile(configurationManager, file)) { + return []; + } + + await configurationManager.configureGloballyFromDocument(file); + + const start = document.offsetAt(range.start); + const length = document.offsetAt(range.end) - start; + + const response = await tspClient.request(CommandTypes.ProvideInlayHints, { file, start, length }); + if (response.type !== 'response' || !response.success || !response.body) { + return []; + } + + return response.body.map(hint => ({ + kind: fromProtocolInlayHintKind(hint.kind), + label: hint.text, + paddingLeft: hint.whitespaceBefore ?? false, + paddingRight: hint.whitespaceAfter ?? false, + position: Position.fromLocation(hint.position) + })); + } +} + +function areInlayHintsEnabledForFile(configurationManager: ConfigurationManager, filename: string) { + const preferences = configurationManager.getPreferences(filename); + + // Doesn't need to include `includeInlayVariableTypeHintsWhenTypeMatchesName` as it depends + // on `includeInlayVariableTypeHints` being enabled. + return preferences.includeInlayParameterNameHints === 'literals' || + preferences.includeInlayParameterNameHints === 'all' || + preferences.includeInlayEnumMemberValueHints || + preferences.includeInlayFunctionLikeReturnTypeHints || + preferences.includeInlayFunctionParameterTypeHints || + preferences.includeInlayPropertyDeclarationTypeHints || + preferences.includeInlayVariableTypeHints; +} + +function fromProtocolInlayHintKind(kind: tsp.InlayHintKind): lsp.InlayHintKind | undefined { + switch (kind) { + case 'Parameter': return lsp.InlayHintKind.Parameter; + case 'Type': return lsp.InlayHintKind.Type; + case 'Enum': return undefined; + default: return undefined; + } +} diff --git a/src/lsp-connection.ts b/src/lsp-connection.ts index 127304f4..6a3f7d62 100644 --- a/src/lsp-connection.ts +++ b/src/lsp-connection.ts @@ -56,11 +56,12 @@ export function createLspConnection(options: IServerOptions): lsp.Connection { connection.onSignatureHelp(server.signatureHelp.bind(server)); connection.onWorkspaceSymbol(server.workspaceSymbol.bind(server)); connection.onFoldingRanges(server.foldingRanges.bind(server)); + connection.languages.inlayHint.on(server.inlayHints.bind(server)); // proposed `textDocument/calls` request connection.onRequest(lspcalls.CallsRequest.type, server.calls.bind(server)); - connection.onRequest(lspinlayHints.type, server.inlayHints.bind(server)); + connection.onRequest(lspinlayHints.type, server.inlayHintsLegacy.bind(server)); connection.onRequest(lsp.SemanticTokensRequest.type, server.semanticTokensFull.bind(server)); connection.onRequest(lsp.SemanticTokensRangeRequest.type, server.semanticTokensRange.bind(server)); diff --git a/src/lsp-server.spec.ts b/src/lsp-server.spec.ts index b1a65353..cb026756 100644 --- a/src/lsp-server.spec.ts +++ b/src/lsp-server.spec.ts @@ -1937,7 +1937,31 @@ describe('inlayHints', () => { ` }; server.didOpenTextDocument({ textDocument: doc }); - const { inlayHints } = await server.inlayHints({ textDocument: doc }); + const inlayHints = await server.inlayHints({ textDocument: doc, range: lsp.Range.create(0, 0, 4, 0) }); + assert.isDefined(inlayHints); + assert.strictEqual(inlayHints!.length, 1); + assert.deepEqual(inlayHints![0], { + label: ': number', + position: { line: 1, character: 29 }, + kind: lsp.InlayHintKind.Type, + paddingLeft: true, + paddingRight: false + }); + }); + + it('inlayHints (legacy)', async () => { + const doc = { + uri: uri('module.ts'), + languageId: 'typescript', + version: 1, + text: ` + export function foo() { + return 3 + } + ` + }; + server.didOpenTextDocument({ textDocument: doc }); + const { inlayHints } = await server.inlayHintsLegacy({ textDocument: doc }); assert.isDefined(inlayHints); assert.strictEqual(inlayHints.length, 1); assert.strictEqual(inlayHints[0].text, ': number'); diff --git a/src/lsp-server.ts b/src/lsp-server.ts index c914df38..18f66f93 100644 --- a/src/lsp-server.ts +++ b/src/lsp-server.ts @@ -33,6 +33,7 @@ import { computeCallers, computeCallees } from './calls.js'; import { IServerOptions } from './utils/configuration.js'; import { TypeScriptVersion, TypeScriptVersionProvider } from './utils/versionProvider.js'; import { TypeScriptAutoFixProvider } from './features/fix-all.js'; +import { TypeScriptInlayHintsProvider } from './features/inlay-hints.js'; import { SourceDefinitionCommand } from './features/source-definition.js'; import { LspClient } from './lsp-client.js'; import { Position, Range } from './utils/typeConverters.js'; @@ -291,6 +292,7 @@ export class LspServer { ] }, hoverProvider: true, + inlayHintProvider: true, renameProvider: true, referencesProvider: true, signatureHelpProvider: { @@ -1187,7 +1189,12 @@ export class LspServer { return callsResult; } - async inlayHints(params: lspinlayHints.InlayHintsParams): Promise { + async inlayHints(params: lsp.InlayHintParams): Promise { + return await TypeScriptInlayHintsProvider.provideInlayHints( + params.textDocument.uri, params.range, this.documents, this.tspClient, this.options.lspClient, this.configurationManager); + } + + async inlayHintsLegacy(params: lspinlayHints.InlayHintsParams): Promise { const file = uriToPath(params.textDocument.uri); this.logger.log('inlayHints', params, file); if (!file) { From 21c45ea8ec4820c3ab578fe42bf9e9a7b590a5d7 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 20 Aug 2022 14:37:12 +0200 Subject: [PATCH 2/4] add legacy warning --- src/lsp-server.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lsp-server.ts b/src/lsp-server.ts index 18f66f93..2e3cc4d5 100644 --- a/src/lsp-server.ts +++ b/src/lsp-server.ts @@ -1195,6 +1195,10 @@ export class LspServer { } async inlayHintsLegacy(params: lspinlayHints.InlayHintsParams): Promise { + this.options.lspClient.logMessage({ + message: 'Support for experimental "typescript/inlayHints" request is deprecated. Use spec-compliant "textDocument/inlayHint" instead.', + type: lsp.MessageType.Warning + }); const file = uriToPath(params.textDocument.uri); this.logger.log('inlayHints', params, file); if (!file) { From 394d6bfd13796fed3db0452449c01fc5492e6141 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 20 Aug 2022 14:47:38 +0200 Subject: [PATCH 3/4] readme update --- README.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d30d8c47..0dbffc16 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Maintained by a [community of contributors](https://github.com/typescript-langua - [Inlay hints \(`typescript/inlayHints`\) \(experimental\)](#inlay-hints-typescriptinlayhints-experimental) - [Callers and callees \(`textDocument/calls`\) \(experimental\)](#callers-and-callees-textdocumentcalls-experimental) - [Supported Protocol features](#supported-protocol-features) + - [Experimental](#experimental) - [Development](#development) - [Build](#build) - [Test](#test) @@ -503,29 +504,32 @@ interface DefinitionSymbol { ## Supported Protocol features +- [x] textDocument/codeAction +- [x] textDocument/completion (incl. `completion/resolve`) +- [x] textDocument/definition - [x] textDocument/didChange (incremental) - [x] textDocument/didClose - [x] textDocument/didOpen - [x] textDocument/didSave -- [x] textDocument/codeAction -- [x] textDocument/completion (incl. completion/resolve) -- [x] textDocument/definition - [x] textDocument/documentHighlight - [x] textDocument/documentSymbol - [x] textDocument/executeCommand -- [x] textDocument/inlayHint (no support for `inlayHint/resolve` or `workspace/inlayHint/refresh`) - [x] textDocument/formatting -- [x] textDocument/rangeFormatting - [x] textDocument/hover -- [x] textDocument/rename +- [x] textDocument/inlayHint (no support for `inlayHint/resolve` or `workspace/inlayHint/refresh`) +- [x] textDocument/rangeFormatting - [x] textDocument/references +- [x] textDocument/rename - [x] textDocument/signatureHelp -- [x] textDocument/calls (experimental) -- [x] typescript/inlayHints (experimental, supported from Typescript v4.4.2) DEPRECATED (use `textDocument/inlayHint` instead) - [x] workspace/symbol - [x] workspace/didChangeConfiguration - [x] workspace/executeCommand +### Experimental + +- [x] textDocument/calls (experimental) +- [x] typescript/inlayHints (experimental, supported from Typescript v4.4.2) DEPRECATED (use `textDocument/inlayHint` instead) + ## Development ### Build From 665914532eb891abff2d28ea8cfd5a3b6e41d1fe Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 20 Aug 2022 16:44:21 +0200 Subject: [PATCH 4/4] don't send undefined values --- src/features/inlay-hints.ts | 16 +++++++++------- src/lsp-server.spec.ts | 3 +-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/features/inlay-hints.ts b/src/features/inlay-hints.ts index 468d4ee7..5de8b5b1 100644 --- a/src/features/inlay-hints.ts +++ b/src/features/inlay-hints.ts @@ -64,13 +64,15 @@ export class TypeScriptInlayHintsProvider { return []; } - return response.body.map(hint => ({ - kind: fromProtocolInlayHintKind(hint.kind), - label: hint.text, - paddingLeft: hint.whitespaceBefore ?? false, - paddingRight: hint.whitespaceAfter ?? false, - position: Position.fromLocation(hint.position) - })); + return response.body.map(hint => { + const inlayHint = lsp.InlayHint.create( + Position.fromLocation(hint.position), + hint.text, + fromProtocolInlayHintKind(hint.kind)); + hint.whitespaceBefore && (inlayHint.paddingLeft = true); + hint.whitespaceAfter && (inlayHint.paddingRight = true); + return inlayHint; + }); } } diff --git a/src/lsp-server.spec.ts b/src/lsp-server.spec.ts index cb026756..e39502d5 100644 --- a/src/lsp-server.spec.ts +++ b/src/lsp-server.spec.ts @@ -1944,8 +1944,7 @@ describe('inlayHints', () => { label: ': number', position: { line: 1, character: 29 }, kind: lsp.InlayHintKind.Type, - paddingLeft: true, - paddingRight: false + paddingLeft: true }); });