diff --git a/README.md b/README.md index ad2a1551..fd4650a8 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ The language server accepts various settings through the `initializationOptions` | plugins | object[] | An array of `{ name: string, location: string }` objects for registering a Typescript plugins. **Default**: [] | | preferences | object | Preferences passed to the Typescript (`tsserver`) process. See below for more info. | -The `preferences` object is an object specifying preferences for the internal `tsserver` process. Those options depend on the version of Typescript used but at the time of writing Typescript v4.3.4 contains these options: +The `preferences` object is an object specifying preferences for the internal `tsserver` process. Those options depend on the version of Typescript used but at the time of writing Typescript v4.4.3 contains these options: ```ts interface UserPreferences { @@ -85,6 +85,7 @@ interface UserPreferences { * values, with insertion text to replace preceding `.` tokens with `?.`. */ includeAutomaticOptionalChainCompletions: boolean; + allowIncompleteCompletions: boolean; importModuleSpecifierPreference: "shortest" | "project-relative" | "relative" | "non-relative"; /** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */ importModuleSpecifierEnding: "auto" | "minimal" | "index" | "js"; @@ -129,6 +130,42 @@ From the `preferences` options listed above, this server explicilty sets the fol - [x] textDocument/signatureHelp - [x] workspace/symbol +## `typescript/inlayHints` (experimental, supported from Typescript v4.4.2) + +```ts +type Request = { + textDocument: TextDocumentIdentifier, + range?: Range, +} + +type Response = { + inlayHints: InlayHint[]; +} + +type InlayHint = { + text: string; + position: lsp.Position; + kind: 'Type' | 'Parameter' | 'Enum'; + whitespaceBefore?: boolean; + whitespaceAfter?: boolean; +}; +``` + +For the request to return any results, some or all of the following options need to be enabled through `preferences`: + +```ts +// Not officially part of UserPreferences yet but you can send them along with the UserPreferences just fine: +export interface InlayHintsOptions extends UserPreferences { + includeInlayParameterNameHints: "none" | "literals" | "all"; + includeInlayParameterNameHintsWhenArgumentMatchesName: boolean; + includeInlayFunctionParameterTypeHints: boolean, + includeInlayVariableTypeHints: boolean; + includeInlayPropertyDeclarationTypeHints: boolean; + includeInlayFunctionLikeReturnTypeHints: boolean; + includeInlayEnumMemberValueHints: boolean; +} +``` + # Development ### Build diff --git a/package.json b/package.json index 8de18715..1b1f6a22 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,6 @@ "rimraf": "^3.0.2", "source-map-support": "^0.5.19", "ts-node": "7.0.1", - "typescript": "^4.3.4" + "typescript": "^4.4.3" } } diff --git a/src/lsp-connection.ts b/src/lsp-connection.ts index d49ee3a6..8952b173 100644 --- a/src/lsp-connection.ts +++ b/src/lsp-connection.ts @@ -7,6 +7,7 @@ import * as lsp from 'vscode-languageserver/node'; import * as lspcalls from './lsp-protocol.calls.proposed'; +import * as lspinlayHints from './lsp-protocol.inlayHints.proposed'; import { LspClientLogger } from './logger'; import { LspServer } from './lsp-server'; @@ -59,5 +60,7 @@ export function createLspConnection(options: IServerOptions): lsp.Connection { // proposed `textDocument/calls` request connection.onRequest(lspcalls.CallsRequest.type, server.calls.bind(server)); + connection.onRequest(lspinlayHints.type, server.inlayHints.bind(server)); + return connection; } diff --git a/src/lsp-protocol.inlayHints.proposed.ts b/src/lsp-protocol.inlayHints.proposed.ts new file mode 100644 index 00000000..4737b171 --- /dev/null +++ b/src/lsp-protocol.inlayHints.proposed.ts @@ -0,0 +1,30 @@ +import * as lsp from 'vscode-languageserver/node'; +import tsp from 'typescript/lib/protocol'; +import { RequestHandler } from 'vscode-jsonrpc'; + +export type InlayHintsParams = { + /** + * The document to format. + */ + textDocument: lsp.TextDocumentIdentifier; + /** + * The range to format + */ + range?: lsp.Range; +}; + +type InlayHint = { + text: string; + position: lsp.Position; + kind: tsp.InlayHintKind; + whitespaceBefore?: boolean; + whitespaceAfter?: boolean; +}; + +export type InlayHintsResult = { + inlayHints: InlayHint[]; +}; + +export const type = new lsp.RequestType('typescript/inlayHints'); + +export type HandlerSignature = RequestHandler; diff --git a/src/lsp-server.spec.ts b/src/lsp-server.spec.ts index b760d72c..af143129 100644 --- a/src/lsp-server.spec.ts +++ b/src/lsp-server.spec.ts @@ -934,3 +934,42 @@ describe('diagnostics (no client support)', () => { assert.isUndefined(resultsForFile, 'Unexpected diagnostics received'); }).timeout(10000); }); + +describe('inlayHints', () => { + it('inlayHints', async () => { + const doc = { + uri: uri('module.ts'), + languageId: 'typescript', + version: 1, + text: ` + export function foo() { + return 3 + } + ` + }; + server.initialize({ + initializationOptions: { + preferences: { + includeInlayFunctionLikeReturnTypeHints: true + } + }, + processId: null, + capabilities: getDefaultClientCapabilities(), + workspaceFolders: [], + rootUri: '' + }); + server.didOpenTextDocument({ + textDocument: doc + }); + + const { inlayHints } = await server.inlayHints({ + textDocument: doc + }); + + assert.isDefined(inlayHints); + assert.strictEqual(inlayHints.length, 1); + assert.strictEqual(inlayHints[0].text, ': number'); + assert.strictEqual(inlayHints[0].kind, 'Type'); + assert.deepStrictEqual(inlayHints[0].position, { line: 1, character: 29 }); + }).timeout(10000); +}); diff --git a/src/lsp-server.ts b/src/lsp-server.ts index 33117233..50974f6d 100644 --- a/src/lsp-server.ts +++ b/src/lsp-server.ts @@ -9,6 +9,7 @@ import * as path from 'path'; import tempy from 'tempy'; import * as lsp from 'vscode-languageserver/node'; import * as lspcalls from './lsp-protocol.calls.proposed'; +import * as lspinlayHints from './lsp-protocol.inlayHints.proposed'; import tsp from 'typescript/lib/protocol'; import * as fs from 'fs-extra'; import * as commandExists from 'command-exists'; @@ -983,4 +984,52 @@ export class LspServer { } return callsResult; } + + async inlayHints(params: lspinlayHints.InlayHintsParams): Promise { + const file = uriToPath(params.textDocument.uri); + this.logger.log('inlayHints', params, file); + if (!file) { + return { inlayHints: [] }; + } + + const doc = this.documents.get(file); + if (!doc) { + return { inlayHints: [] }; + } + + const start = doc.offsetAt(params.range?.start ?? { + line: 0, + character: 0 + }); + const end = doc.offsetAt(params.range?.end ?? { + line: doc.lineCount + 1, + character: 0 + }); + + try { + const result = await this.tspClient.request( + CommandTypes.ProvideInlayHints, + { + file, + start: start, + length: end - start + } + ); + + return { + inlayHints: + result.body?.map((item) => ({ + text: item.text, + position: toPosition(item.position), + whitespaceAfter: item.whitespaceAfter, + whitespaceBefore: item.whitespaceBefore, + kind: item.kind + })) ?? [] + }; + } catch { + return { + inlayHints: [] + }; + } + } } diff --git a/src/tsp-client.spec.ts b/src/tsp-client.spec.ts index 6e0b8e54..4cdec883 100644 --- a/src/tsp-client.spec.ts +++ b/src/tsp-client.spec.ts @@ -65,6 +65,30 @@ for (const [serverName, server] of Object.entries({ executableServer, moduleServ assert.equal(references.body!.symbolName, 'doStuff'); }).timeout(10000); + it('inlayHints', async () => { + const f = filePath('module2.ts'); + server.notify(CommandTypes.Open, { + file: f, + fileContent: readContents(f) + }); + await server.request(CommandTypes.Configure, { + preferences: { + // @ts-expect-error preference exist + includeInlayFunctionLikeReturnTypeHints: true + } + }); + const inlayHints = await server.request( + CommandTypes.ProvideInlayHints, + { + file: f, + start: 0, + length: 1000 + } + ); + assert.isDefined(inlayHints.body); + assert.equal(inlayHints.body![0].text, ': boolean'); + }).timeout(10000); + it('documentHighlight', async () => { const f = filePath('module2.ts'); server.notify(CommandTypes.Open, { diff --git a/src/tsp-client.ts b/src/tsp-client.ts index 9a316295..9a453ef8 100644 --- a/src/tsp-client.ts +++ b/src/tsp-client.ts @@ -61,6 +61,7 @@ interface TypeScriptRequestTypes { 'rename': [protocol.RenameRequestArgs, protocol.RenameResponse]; 'signatureHelp': [protocol.SignatureHelpRequestArgs, protocol.SignatureHelpResponse]; 'typeDefinition': [protocol.FileLocationRequestArgs, protocol.TypeDefinitionResponse]; + 'provideInlayHints': [protocol.InlayHintsRequestArgs, protocol.InlayHintsResponse]; } export class TspClient { diff --git a/src/tsp-command-types.ts b/src/tsp-command-types.ts index 1187518a..f2f8d607 100644 --- a/src/tsp-command-types.ts +++ b/src/tsp-command-types.ts @@ -85,7 +85,8 @@ export const enum CommandTypes { UncommentSelection = 'uncommentSelection', PrepareCallHierarchy = 'prepareCallHierarchy', ProvideCallHierarchyIncomingCalls = 'provideCallHierarchyIncomingCalls', - ProvideCallHierarchyOutgoingCalls = 'provideCallHierarchyOutgoingCalls' + ProvideCallHierarchyOutgoingCalls = 'provideCallHierarchyOutgoingCalls', + ProvideInlayHints = 'provideInlayHints', } export const enum EventTypes { diff --git a/yarn.lock b/yarn.lock index 0567a1fb..33e48e0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1624,10 +1624,10 @@ type-fest@^0.6.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== -typescript@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.4.tgz#3f85b986945bcf31071decdd96cf8bfa65f9dcbc" - integrity sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew== +typescript@^4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.3.tgz#bdc5407caa2b109efd4f82fe130656f977a29324" + integrity sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA== unique-string@^2.0.0: version "2.0.0"