diff --git a/Readme.md b/Readme.md index 4d97407..b6ba5f4 100644 --- a/Readme.md +++ b/Readme.md @@ -41,7 +41,7 @@ tsserver understand your code. installation. **Note:** tsserver could be quite slow to initialize on big project, exclude -unneunnecessary files in your jsconfig.json/tsconfig.json. +unnecessary files in your jsconfig.json/tsconfig.json. **Note:** if you're using WSL, copy you project files from mounted dirs to linux home otherwise tsserver will not work properly. @@ -100,10 +100,19 @@ Almost the same as VSCode. - Code refactor using code actions. - Find references. - Signature help. +- Call hierarchy. +- Selection range. +- Semantic tokens. - Rename symbols support. +- Automatic tag closing. - Rename imports on file rename, require [watchman](https://facebook.github.io/watchman/) installed in your \$PATH. - Search for workspace symbols. +- Inlay hints support using virtual text feature of neovim, which requires: + - TypeScript >= 4.4.0 + - Neovim >= 0.4.0 + - Enabled by options starts with `typescript.inlayHints` or + `javascript.inlayHints`. Tsserver module first resolved from your local workspace. If it's not found, use tsserver from `tsserver.tsdk` configuration or use bundled tsserver with this @@ -117,6 +126,8 @@ for guide of coc.nvim's configuration. - `tsserver.enable`:Enable tsserver extension, default: `true` - `tsserver.locale`:Locale of tsserver, default: `""` +- `tsserver.ignoreLocalTsserver`:Always use tsserver module from tsserver.tsdk + or coc-tsserver extension. - `tsserver.typingsCacheLocation`:Folder path for cache typings, default: `""` - `tsserver.formatOnType`:Run format on type special characters., default: `true` @@ -128,10 +139,9 @@ for guide of coc.nvim's configuration. - `tsserver.log`:Log level of tsserver, default: `"off"` - `tsserver.trace.server`:Trace level of tsserver, default: `"off"` - `tsserver.pluginPaths`:Folders contains tsserver plugins, default: `[]` -- `tsserver.debugPort`:Debug port number of tsserver - `tsserver.watchOptions`:Configure which watching strategies should be used to keep track of files and directories. Requires using TypeScript 3.8+ in the - workspace, default: undefined. + workspace, default: `undefined` - `tsserver.reportStyleChecksAsWarnings` default: `true` - `tsserver.implicitProjectConfig.checkJs`:Enable checkJs for implicit project, default: `false` @@ -139,27 +149,38 @@ for guide of coc.nvim's configuration. experimentalDecorators for implicit project, default: `false` - `tsserver.disableAutomaticTypeAcquisition`:Disable download of typings, default: `false` -- `tsserver.useBatchedBufferSync`: use batched buffer synchronize support. +- `tsserver.useBatchedBufferSync`: use batched buffer synchronize support, default: `true` +- `tsserver.enableTracing`: Enables tracing TS server performance to a + directory. These trace files can be used to diagnose TS Server performance + issues. The log may contain file paths, source code, and other potentially + sensitive information from your project, default: `false` +- `typescript.check.npmIsInstalled`: Check if npm is installed for [Automatic + Type + Acquisition](https://code.visualstudio.com/docs/nodejs/working-with-javascript#_typings-and-automatic-type-acquisition). - `typescript.updateImportsOnFileMove.enable`:Enable update imports on file move., default: `true` - `typescript.implementationsCodeLens.enable`:Enable codeLens for implementations, default: `true` - `typescript.referencesCodeLens.enable`:Enable codeLens for references, default: `true` -- `typescript.preferences.importModuleSpecifier` default: `"auto"` +- `typescript.referencesCodeLens.showOnAllFunctions`: Enable/disable references CodeLens on all functions in typescript files. Default: `false` +- `typescript.preferences.importModuleSpecifier` default: `"shortest"` - `typescript.preferences.importModuleSpecifierEnding` default: `"auto"` - `typescript.preferences.quoteStyle` default: `"single"` +- `typescript.preferences.includePackageJsonAutoImports`: Enable/disable + searching `package.json` dependencies for available auto imports, default: + `"auto"` - `typescript.suggestionActions.enabled`:Enable/disable suggestion diagnostics for TypeScript files in the editor. Requires using TypeScript 2.8 or newer in the workspace., default: `true` - `typescript.validate.enable`:Enable/disable TypeScript validation., default: `true` - `typescript.showUnused`: show unused variable hint, default: `true`. -- `typescript.autoClosingTags`: Enable/disable autoClosing of JSX tags, default: `false`. +- `typescript.autoClosingTags`: Enable/disable autoClosing of JSX tags, default: `true` - `typescript.suggest.enabled` default: `true` - `typescript.suggest.paths`:Enable/disable suggest paths in import statement and require calls, default: `true` -- `typescript.suggest.autoImports`:Enable/disable auto import suggests., +- `typescript.suggest.autoImports`:Enable/disable auto import suggests, default: `true` - `typescript.suggest.completeFunctionCalls`:Enable snippet for method suggestion, default: `true` @@ -168,6 +189,13 @@ for guide of coc.nvim's configuration. TypeScript 4.3+ in the workspace, default: `true` - `typescript.suggest.includeCompletionsWithSnippetText`: Enable snippet completions from TS Server. Requires using TypeScript 4.3+ in the workspace, default: `true` +- `typescript.suggest.classMemberSnippets.enabled`: Enable/disable + snippet completions for class members. Requires using TypeScript 4.5+ in the + workspace, default: `true` +- `typescript.suggest.jsdoc.generateReturns`: Enable/disable generating + `@return` annotations for JSDoc templates. Requires using TypeScript 4.2+ in + the workspace. default: `true` +- `typescript.suggest.includeAutomaticOptionalChainCompletions`: default: `true` - `typescript.format.enabled`:Enable/disable format of typescript files. - `typescript.format.insertSpaceAfterCommaDelimiter` default: `true` - `typescript.format.insertSpaceAfterConstructor` default: `false` @@ -193,14 +221,15 @@ for guide of coc.nvim's configuration. - `typescript.format.insertSpaceAfterTypeAssertion` default: `false` - `typescript.format.placeOpenBraceOnNewLineForFunctions` default: `false` - `typescript.format.placeOpenBraceOnNewLineForControlBlocks` default: `false` -- `typescript.suggest.includeAutomaticOptionalChainCompletions`: default: `true` -- `javascript.format.enabled`: Enable/disable format for javascript files. -- `javascript.showUnused`: show unused variable hint. -- `javascript.autoClosingTags`: Enable/disable autoClosing of JSX tags, default: `false`. +- `typescript.inlayHints`: inlayHints related options. +- `javascript.format.enabled`: Enable/disable format for javascript files, default: `true` +- `javascript.showUnused`: show unused variable hint, default: `true` +- `javascript.autoClosingTags`: Enable/disable autoClosing of JSX tags, default: `true` - `javascript.updateImportsOnFileMove.enable` default: `true` - `javascript.implementationsCodeLens.enable` default: `true` - `javascript.referencesCodeLens.enable` default: `true` -- `javascript.preferences.importModuleSpecifier` default: `"auto"` +- `javascript.referencesCodeLens.showOnAllFunctions`: Enable/disable references CodeLens on all functions in JavaScript files default: `false` +- `javascript.preferences.importModuleSpecifier` default: `"shortest"` - `javascript.preferences.importModuleSpecifierEnding` default: `"auto"` - `javascript.preferences.quoteStyle` default: `"single"` - `javascript.validate.enable`: Enable/disable JavaScript validation., default: @@ -219,6 +248,13 @@ for guide of coc.nvim's configuration. - `javascript.suggest.includeCompletionsForImportStatements`: Enable/disable auto-import-style completions on partially-typed import statements. Requires using TypeScript 4.3+ in the workspace, default: `true` +- `javascript.suggest.jsdoc.generateReturns`: Enable/disable generating + `@return` annotations for JSDoc templates. Requires using TypeScript 4.2+ in + the workspace. default: `true` +- `javascript.suggest.classMemberSnippets.enabled`: Enable/disable + snippet completions for class members. Requires using TypeScript 4.5+ in the + workspace, default: `true` +- `javascript.suggest.includeAutomaticOptionalChainCompletions`: default: `true` - `javascript.format.insertSpaceAfterCommaDelimiter` default: `true` - `javascript.format.insertSpaceAfterConstructor` default: `false` - `javascript.format.insertSpaceAfterSemicolonInForStatements` default: `true` @@ -243,10 +279,22 @@ for guide of coc.nvim's configuration. - `javascript.format.insertSpaceAfterTypeAssertion` default: `false` - `javascript.format.placeOpenBraceOnNewLineForFunctions` default: `false` - `javascript.format.placeOpenBraceOnNewLineForControlBlocks` default: `false` -- `javascript.suggest.includeAutomaticOptionalChainCompletions`: default: `true` +- `javascript.inlayHints`: inlayHints related options. -Configurations are the same as with VSCode. Try completion with `tsserver`, -`typescript` or `javascript` in your `coc-settings.json`. +### Added on 1.10.0 + +- `javascript.suggest.completeJSDocs` `typescript.suggest.completeJSDocs`: + Enable/disable suggestion to complete JSDoc comments. default: `true` + +### Added on 1.10.1 +- `typescript.suggest.objectLiteralMethodSnippets.enabled` + `javascript.suggest.objectLiteralMethodSnippets.enabled`: + Enable/disable snippet completions for methods in object literals. Requires using TypeScript 4.7+ in the workspace + +Configurations are the same as with VSCode. Install +[coc-json](https://github.com/neoclide/coc-json) and try completion with +`tsserver`, `typescript` or `javascript` in your +`coc-settings.json`. ## Related extensions diff --git a/bin/tsserverForkStart.js b/bin/tsserverForkStart.js deleted file mode 100644 index 22abfa6..0000000 --- a/bin/tsserverForkStart.js +++ /dev/null @@ -1,161 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -let net = require('net'); -let fs = require('fs'); -let ENABLE_LOGGING = false; -let log = (function () { - if (!ENABLE_LOGGING) { - return function () { }; // tslint:disable-line - } - let isFirst = true; - let LOG_LOCATION = 'C:\\stdFork.log'; - return function log(str) { - if (isFirst) { - isFirst = false; - fs.writeFileSync(LOG_LOCATION, str + '\n'); - return; - } - fs.appendFileSync(LOG_LOCATION, str + '\n'); - }; -})(); -let stdInPipeName = process.env['STDIN_PIPE_NAME']; // tslint:disable-line -let stdOutPipeName = process.env['STDOUT_PIPE_NAME']; // tslint:disable-line -let stdErrPipeName = process.env['STDERR_PIPE_NAME']; // tslint:disable-line -log('STDIN_PIPE_NAME: ' + stdInPipeName); -log('STDOUT_PIPE_NAME: ' + stdOutPipeName); -log('STDERR_PIPE_NAME: ' + stdErrPipeName); -(function () { - log('Beginning stdout redirection...'); - // Create a writing stream to the stdout pipe - let stdOutStream = net.connect(stdOutPipeName); - // unref stdOutStream to behave like a normal standard out - stdOutStream.unref(); - process.__defineGetter__('stdout', function () { - return stdOutStream; - }); - // Create a writing stream to the stderr pipe - let stdErrStream = net.connect(stdErrPipeName); - // unref stdErrStream to behave like a normal standard out - stdErrStream.unref(); - process.__defineGetter__('stderr', function () { - return stdErrStream; - }); - let fsWriteSyncString = function (// tslint:disable-line - fd, str, _position, encoding) { - // fs.writeSync(fd, string[, position[, encoding]]) - let buf = Buffer.from(str, encoding || 'utf8'); - return fsWriteSyncBuffer(fd, buf, 0, buf.length); // tslint:disable-line - }; - let fsWriteSyncBuffer = function (// tslint:disable-line - fd, buffer, off, len) { - off = Math.abs(off | 0); - len = Math.abs(len | 0); - // fs.writeSync(fd, buffer, offset, length[, position]) - let buffer_length = buffer.length; - if (off > buffer_length) { - throw new Error('offset out of bounds'); - } - if (len > buffer_length) { - throw new Error('length out of bounds'); - } - if (((off + len) | 0) < off) { - throw new Error('off + len overflow'); - } - if (buffer_length - off < len) { - // Asking for more than is left over in the buffer - throw new Error('off + len > buffer.length'); - } - let slicedBuffer = buffer; - if (off !== 0 || len !== buffer_length) { - slicedBuffer = buffer.slice(off, off + len); - } - if (fd === 1) { - stdOutStream.write(slicedBuffer); - } - else { - stdErrStream.write(slicedBuffer); - } - return slicedBuffer.length; - }; - // handle fs.writeSync(1, ...) - let originalWriteSync = fs.writeSync; - fs.writeSync = function (// tslint:disable-line - fd, data, _position, _encoding) { - if (fd !== 1 && fd !== 2) { - return originalWriteSync.apply(fs, arguments); - } - // usage: - // fs.writeSync(fd, buffer, offset, length[, position]) - // OR - // fs.writeSync(fd, string[, position[, encoding]]) - if (data instanceof Buffer) { - return fsWriteSyncBuffer.apply(null, arguments); - } - // For compatibility reasons with fs.writeSync, writing null will write "null", etc - if (typeof data !== 'string') { - data += ''; - } - return fsWriteSyncString.apply(null, arguments); - }; - log('Finished defining process.stdout, process.stderr and fs.writeSync'); -})(); -(function () { - // Begin listening to stdin pipe - let server = net.createServer(function (stream) { - // Stop accepting new connections, keep the existing one alive - server.close(); - log('Parent process has connected to my stdin. All should be good now.'); - process.__defineGetter__('stdin', function () { - return stream; - }); - // Remove myself from process.argv - process.argv.splice(1, 1); - // Load the actual program - let program = process.argv[1]; - log('Loading program: ' + program); - // Unset the custom environmental variables that should not get inherited - delete process.env['STDIN_PIPE_NAME']; // tslint:disable-line - delete process.env['STDOUT_PIPE_NAME']; // tslint:disable-line - delete process.env['STDERR_PIPE_NAME']; // tslint:disable-line - require(program); - log('Finished loading program.'); - let stdinIsReferenced = true; - let timer = setInterval(function () { - let listenerCount = stream.listeners('data').length + - stream.listeners('end').length + - stream.listeners('close').length + - stream.listeners('error').length; - // log('listenerCount: ' + listenerCount) - if (listenerCount <= 1) { - // No more "actual" listeners, only internal node - if (stdinIsReferenced) { - stdinIsReferenced = false; - // log('unreferencing stream!!!') - stream.unref(); - } - } - else { - // There are "actual" listeners - if (!stdinIsReferenced) { - stdinIsReferenced = true; - stream.ref(); - } - } - // log( - // '' + stream.listeners('data').length + - // ' ' + stream.listeners('end').length + - // ' ' + stream.listeners('close').length + - // ' ' + stream.listeners('error').length - // ) - }, 1000); - if (timer.unref) { // tslint:disable-line - timer.unref(); // tslint:disable-line - } - }); - server.listen(stdInPipeName, function () { - // signal via stdout that the parent process can now begin writing to stdin pipe - process.stdout.write('ready'); - }); -})(); diff --git a/history.md b/history.md new file mode 100644 index 0000000..a9a19c7 --- /dev/null +++ b/history.md @@ -0,0 +1,260 @@ +# 1.10.5 + +- Fix a fold issue #380 + +# 1.10.2 + +- Fix snippet completion not work for optional complete item. + +# 1.10.1 + +- Avoid unnecessary fetch of format option. +- Add `typescript.suggest.objectLiteralMethodSnippets.enabled` + +# 1.10.0 + +- Support jsdoc completion. +- Add configurations `javascript.suggest.completeJSDocs` and `typescript.suggest.completeJSDocs`. + +# 1.9.15 + +- Fix uri for `zipfile`. + +# 1.9.14 + +- Add javascript snippets +- Fix command `tsserver.restart` not work + +# 1.9.11 + +- Resued resolved tsserver path after `:CocRestart` + +# 1.9.10 + +- Watch for `tsserver.enable` configuration to change service state. +- Fix tsserver not work well with `:CocList services` + +# 1.9.9 + +- Use documentChanges for workspaceEdit. + +# 1.9.8 + +- Log to output when document content exceed limit of semantic tokens. + +# 1.9.7 + +- Change default of `javascript.autoClosingTags` and `typescript.autoClosingTags` to `true`. + +# 1.9.6 + +- Rework codeLens related. + +# 1.9.5 + +- Change 'allImportsAreUnused' diagnostic kind to warning. + +# 1.9.4 + +- Improve file pattern for config file. + +# 1.9.2 + +- Inlay hints support (#335) + +# 1.9.1 + +- use `TSS_DEBUG` & `TSS_DEBUG_BRK` for debug port + +# 1.9.0 + +- Add semanticTokens support #313 +- Add jsxAttributeCompletionStyle settings #319 +- Add command `tsserver.sortImports` #322 +- Add suggest.classMemberSnippets.enabled configuration cd16da8 +- Add suggest.jsdoc.generateReturns configuration 5a8c68f +- Add typescript.preferences.includePackageJsonAutoImports configuration 4d78b61 +- Add tsserver.enableTracing configuration 43e6f62 +- Add typescript.check.npmIsInstalled configuration 3bd84b1 + +# 1.8.3 + +- Support deprecated tag for document symbols, diagnostic, workspace symbols. + +# 1.8.2 + +- Support call hierarchy. +- Support `tags` and access modifier for document symbols. +- Support return `DefinitionLink[]` for definition provider. + +# 1.8.1 + +- Support `tsserver.tsconfigPath` configuration. + +# 1.8.0 + +- Support [Import Statement Completions](https://devblogs.microsoft.com/typescript/announcing-typescript-4-3/#import-statement-completions) + +# 1.7.0 + +- Support tag closing for JSX + +# 1.6.4 + +- Support `typescript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces` and `ypescript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces` + +# 1.6.2 + +- Support languages from plugins. + +# 1.5.5 + +- Support `typescript.preferences.useAliasesForRenames` and `javascript.preferences.useAliasesForRenames` + +# 1.5.3 + +- Support the new path of Yarn v2 pnpify SDK. +- Us `tsserver.pluginPaths` replace `tsserver.pluginRoot`. + +# 1.5.0 + +- Support @ts-expect-error directive on tsserver v390. +- Support `tsserver.watchOptions` configuration. + +# 1.4.13 + +- Add `preferences.importModuleSpecifierEnding` configuration. +- Change `preferences.importModuleSpecifier` default to `auto`. + +# 1.4.12 + +- Support `tsserver.maxTsServerMemory` configuration. + +# 1.4.9 + +- Support semicolons format option. + +# 1.4.8 + +- support `format.enabled` configuration + +# 1.4.3 + +- Use global tsc when local tsc not foun + +# 1.4.0 + +- remove noSemicolons preferences + +# 1.3.15 + +- Add missing option "auto" to importModuleSpecifier + +# 1.3.11 + +- Add `tsserver.ignoreLocalTsserver` configuration. + +# 1.3.6 + +- Support `b:coc_tsserver_disable` + +# 1.3.2 + +- fix suggestionActions.enabled configuration not working + +# 1.3.1 + +- fix validate.enable not work sometimes + +# 1.3.0 + +- Loading status. +- Batched buffer synchronize. +- Configuration for showUnused variable. +- Smart selection support. +- Support 'auto' as quoteStyle. +- Support 'validateDefaultNpmLocation'. + +# 1.1.30 + +- rework of typescriptService, support interuptGetErr + +# 1.1.29 + +- Support plugin feature. + +# 1.1.28 + +- add codeAction provider for import missing node builtin modules. + +# 1.1.26 + +- Add install module codeAction for module not found diagnostic. +- Rework `tsserver.watchBuild`, use background process, support statusline. + +# 1.1.25 + +- Support autofix of node modules import + +# 1.1.23 + +- Add command `tsserver.executeAutofix` + +# 1.1.13 + +- Add triggerCharacters for SignatureHelp + +# 1.1.12 + +- Add typescript snippets from VSCode + +# 1.1.11 + +- Fix throw error of "No content available" on completion. + +# 1.1.10 + +- Support projectRootPath for document + +# 1.1.9 + +- Support commitCharacters of completion items + +# 1.1.8 + +- Add status bar support. + +# 1.1.7 + +- Add settings `javascript.validate.enable` and `typescript.validate.enable` + +# 1.1.6 + +- Fix suggestionActions.enabled not works + +# 1.1.5 + +- Use quickfix list for watchBuild errors + +# 1.1.4 + +- Fix organizeImports not working sometimes + +# 1.1.3 + +- Remove settings with `commaAfterImport`, use `typescript.preferences.noSemicolons` and `javasscript.preferences.noSemicolons` instead. + +# 1.1.2 + +- Support diagnostic of config file. + +# 1.1.1 + +- Remove unnecessary use of workspace terminal. + +# 1.1.0 + +- Support rename import path: https://code.visualstudio.com/updates/v1_28#_rename-import-path +- Use new `suggest` for completion configuration: https://code.visualstudio.com/updates/v1_28#_new-settings-for-jsts-suggestions +- Convert to async function: https://code.visualstudio.com/updates/v1_28#_convert-to-async-function +- Remove semicolons on format: set `typescript.preferences.noSemicolons` to true diff --git a/package.json b/package.json index b09a907..749c97c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coc-tsserver", - "version": "1.8.6", + "version": "1.10.5", "description": "tsserver extension for coc.nvim", "main": "lib/index.js", "publisher": "chemzqm", @@ -224,6 +224,12 @@ ], "description": "Trace level of tsserver" }, + "tsserver.enableTracing": { + "type": "boolean", + "default": false, + "description": "Enables tracing TS server performance to a directory. These trace files can be used to diagnose TS Server performance issues. The log may contain file paths, source code, and other potentially sensitive information from your project.", + "scope": "window" + }, "tsserver.pluginPaths": { "type": "array", "default": [], @@ -232,10 +238,6 @@ }, "description": "Folders contains tsserver plugins" }, - "tsserver.debugPort": { - "type": "number", - "description": "Debug port number of tsserver" - }, "tsserver.reportStyleChecksAsWarnings": { "type": "boolean", "default": true @@ -260,6 +262,12 @@ "default": true, "description": "Use batched buffer sync support." }, + "typescript.check.npmIsInstalled": { + "type": "boolean", + "default": true, + "markdownDescription": "Check if npm is installed for [Automatic Type Acquisition](https://code.visualstudio.com/docs/nodejs/working-with-javascript#_typings-and-automatic-type-acquisition).", + "scope": "window" + }, "typescript.showUnused": { "type": "boolean", "default": true, @@ -285,6 +293,12 @@ "default": true, "description": "Enable codeLens for references" }, + "typescript.referencesCodeLens.showOnAllFunctions": { + "type": "boolean", + "default": false, + "description": "Enable/disable references CodeLens on all functions in typescript files.", + "scope": "window" + }, "typescript.preferences.importModuleSpecifier": { "type": "string", "default": "shortest", @@ -314,6 +328,38 @@ "description": "Preferred path ending for auto imports.", "scope": "resource" }, + "typescript.preferences.jsxAttributeCompletionStyle": { + "type": "string", + "enum": [ + "auto", + "braces", + "none" + ], + "markdownEnumDescriptions": [ + "Insert `={}` or `=\"\"` after attribute names based on the prop type.", + "Insert `={}` after attribute names.", + "Only insert attribute names." + ], + "default": "auto", + "description": "Preferred style for JSX attribute completions.", + "scope": "resource" + }, + "typescript.preferences.includePackageJsonAutoImports": { + "type": "string", + "enum": [ + "auto", + "on", + "off" + ], + "enumDescriptions": [ + "Search dependencies based on estimated performance impact.", + "Always search dependencies.", + "Never search dependencies." + ], + "default": "auto", + "markdownDescription": "Enable/disable searching `package.json` dependencies for available auto imports.", + "scope": "window" + }, "typescript.preferences.quoteStyle": { "type": "string", "default": "auto", @@ -374,6 +420,18 @@ "description": "Enable/disable snippet completions from TS Server. Requires using TypeScript 4.3+ in the workspace.", "scope": "resource" }, + "typescript.suggest.classMemberSnippets.enabled": { + "type": "boolean", + "default": true, + "description": "Enable/disable snippet completions for class members. Requires using TypeScript 4.5+ in the workspace", + "scope": "resource" + }, + "typescript.suggest.jsdoc.generateReturns": { + "type": "boolean", + "default": true, + "markdownDescription": "Enable/disable generating `@return` annotations for JSDoc templates. Requires using TypeScript 4.2+ in the workspace.", + "scope": "resource" + }, "typescript.format.enabled": { "type": "boolean", "default": true, @@ -471,7 +529,8 @@ }, "typescript.autoClosingTags": { "type": "boolean", - "default": false + "default": true, + "description": "Enable/disable automatic closing of JSX tags." }, "javascript.showUnused": { "type": "boolean", @@ -495,6 +554,12 @@ "type": "boolean", "default": true }, + "javascript.referencesCodeLens.showOnAllFunctions": { + "type": "boolean", + "default": false, + "description": "Enable/disable references CodeLens on all functions in JavaScript files.", + "scope": "window" + }, "javascript.preferences.importModuleSpecifier": { "type": "string", "default": "shortest", @@ -524,6 +589,22 @@ "description": "Preferred path ending for auto imports.", "scope": "resource" }, + "javascript.preferences.jsxAttributeCompletionStyle": { + "type": "string", + "enum": [ + "auto", + "braces", + "none" + ], + "markdownEnumDescriptions": [ + "Insert `={}` or `=\"\"` after attribute names based on the prop type.", + "Insert `={}` after attribute names.", + "Only insert attribute names." + ], + "default": "auto", + "description": "Preferred style for JSX attribute completions.", + "scope": "resource" + }, "javascript.preferences.quoteStyle": { "type": "string", "default": "auto", @@ -578,6 +659,18 @@ "description": "Enable/disable auto-import-style completions on partially-typed import statements. Requires using TypeScript 4.3+ in the workspace.", "scope": "resource" }, + "javascript.suggest.classMemberSnippets.enabled": { + "type": "boolean", + "default": true, + "description": "Enable/disable snippet completions for class members. Requires using TypeScript 4.5+ in the workspace", + "scope": "resource" + }, + "javascript.suggest.jsdoc.generateReturns": { + "type": "boolean", + "default": true, + "markdownDescription": "Enable/disable generating `@return` annotations for JSDoc templates. Requires using TypeScript 4.2+ in the workspace.", + "scope": "resource" + }, "javascript.format.enabled": { "type": "boolean", "default": true, @@ -650,12 +743,117 @@ "javascript.suggest.includeAutomaticOptionalChainCompletions": { "type": "boolean", "default": true, - "description": "%configuration.suggest.includeAutomaticOptionalChainCompletions%", + "description": "Enable/disable showing completions on potentially undefined values that insert an optional chain call. Requires TS 3.7+ and strict null checks to be enabled.", + "scope": "resource" + }, + "typescript.inlayHints.parameterNames.enabled": { + "type": "string", + "enum": [ + "none", + "literals", + "all" + ], + "enumDescriptions": [ + "Disable parameter name hints.", + "Enable parameter name hints only for literal arguments.", + "Enable parameter name hints for literal and non-literal arguments." + ], + "default": "none", + "description": "Enable/disable inlay hints of parameter names.", + "scope": "resource" + }, + "typescript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": { + "type": "boolean", + "default": true, + "description": "Suppress parameter name hints on arguments whose text is identical to the parameter name.", + "scope": "resource" + }, + "typescript.inlayHints.parameterTypes.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable inlay hints of parameter types.", + "scope": "resource" + }, + "typescript.inlayHints.variableTypes.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable inlay hints of variable types.", + "scope": "resource" + }, + "typescript.inlayHints.propertyDeclarationTypes.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable inlay hints of property declarations.", + "scope": "resource" + }, + "typescript.inlayHints.functionLikeReturnTypes.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable inlay hints of return type for function signatures.", + "scope": "resource" + }, + "typescript.inlayHints.enumMemberValues.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable inlay hints of enum member values.", + "scope": "resource" + }, + "javascript.inlayHints.parameterNames.enabled": { + "type": "string", + "enum": [ + "none", + "literals", + "all" + ], + "enumDescriptions": [ + "Disable parameter name hints.", + "Enable parameter name hints only for literal arguments.", + "Enable parameter name hints for literal and non-literal arguments." + ], + "default": "none", + "description": "Enable/disable inlay hints of parameter names.", + "scope": "resource" + }, + "javascript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": { + "type": "boolean", + "default": true, + "description": "Suppress parameter name hints on arguments whose text is identical to the parameter name.", + "scope": "resource" + }, + "javascript.inlayHints.parameterTypes.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable inlay hints of parameter types.", + "scope": "resource" + }, + "javascript.inlayHints.variableTypes.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable inlay hints of variable types.", + "scope": "resource" + }, + "javascript.inlayHints.propertyDeclarationTypes.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable inlay hints of property declarations.", + "scope": "resource" + }, + "javascript.inlayHints.functionLikeReturnTypes.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable inlay hints of return type for function signatures.", + "scope": "resource" + }, + "javascript.inlayHints.enumMemberValues.enabled": { + "type": "boolean", + "default": false, + "description": "Enable/disable inlay hints of enum member values.", "scope": "resource" }, "javascript.autoClosingTags": { "type": "boolean", - "default": false + "default": true, + "description": "Enable/disable automatic closing of JSX tags." }, "javascript.format.semicolons": { "type": "string", @@ -667,6 +865,28 @@ "insert", "remove" ] + }, + "javascript.suggest.completeJSDocs": { + "type": "boolean", + "default": true, + "description": "Enable/disable suggestion to complete JSDoc comments." + }, + "typescript.suggest.completeJSDocs": { + "type": "boolean", + "default": true, + "description": "Enable/disable suggestion to complete JSDoc comments." + }, + "javascript.suggest.objectLiteralMethodSnippets.enabled": { + "type": "boolean", + "default": true, + "description": "Enable/disable snippet completions for methods in object literals. Requires using TypeScript 4.7+ in the workspace", + "scope": "resource" + }, + "typescript.suggest.objectLiteralMethodSnippets.enabled": { + "type": "boolean", + "default": true, + "description": "Enable/disable snippet completions for methods in object literals. Requires using TypeScript 4.7+ in the workspace", + "scope": "resource" } } }, @@ -678,20 +898,28 @@ { "language": "typescriptreact", "path": "./snippets/typescript.json" + }, + { + "language": "javascript", + "path": "./snippets/javascript.json" + }, + { + "language": "javascriptreact", + "path": "./snippets/javascript.json" } ] }, "author": "chemzqm@gmail.com", "license": "MIT", "devDependencies": { - "@types/node": "^10.12.0", - "coc.nvim": "^0.0.81-next.6", - "esbuild": "^0.8.29", - "semver": "^7.3.2", + "@types/node": "^12.12.12", + "coc.nvim": "^0.0.81-next.25", + "esbuild": "^0.14.11", + "semver": "^7.3.5", "vscode-languageserver-protocol": "^3.16.0", "which": "^2.0.2" }, "dependencies": { - "typescript": "^4.3.5" + "typescript": "^4.7.2" } } diff --git a/snippets/javascript.json b/snippets/javascript.json new file mode 100644 index 0000000..ac5cd55 --- /dev/null +++ b/snippets/javascript.json @@ -0,0 +1,194 @@ +{ + "define module": { + "prefix": "define", + "body": [ + "define([", + "\t'require',", + "\t'${1:dependency}'", + "], function(require, ${2:factory}) {", + "\t'use strict';", + "\t$0", + "});" + ], + "description": "define module" + }, + "For Loop": { + "prefix": "for", + "body": [ + "for (let ${1:index} = 0; ${1:index} < ${2:array}.length; ${1:index}++) {", + "\tconst ${3:element} = ${2:array}[${1:index}];", + "\t$TM_SELECTED_TEXT$0", + "}" + ], + "description": "For Loop" + }, + "For-Each Loop": { + "prefix": "foreach", + "body": [ + "${1:array}.forEach(${2:element} => {", + "\t$TM_SELECTED_TEXT$0", + "});" + ], + "description": "For-Each Loop" + }, + "For-In Loop": { + "prefix": "forin", + "body": [ + "for (const ${1:key} in ${2:object}) {", + "\tif (Object.hasOwnProperty.call(${2:object}, ${1:key})) {", + "\t\tconst ${3:element} = ${2:object}[${1:key}];", + "\t\t$TM_SELECTED_TEXT$0", + "\t}", + "}" + ], + "description": "For-In Loop" + }, + "For-Of Loop": { + "prefix": "forof", + "body": [ + "for (const ${1:iterator} of ${2:object}) {", + "\t$TM_SELECTED_TEXT$0", + "}" + ], + "description": "For-Of Loop" + }, + "Function Statement": { + "prefix": "function", + "body": [ + "function ${1:name}(${2:params}) {", + "\t$TM_SELECTED_TEXT$0", + "}" + ], + "description": "Function Statement" + }, + "If Statement": { + "prefix": "if", + "body": [ + "if (${1:condition}) {", + "\t$TM_SELECTED_TEXT$0", + "}" + ], + "description": "If Statement" + }, + "If-Else Statement": { + "prefix": "ifelse", + "body": [ + "if (${1:condition}) {", + "\t$TM_SELECTED_TEXT$0", + "} else {", + "\t", + "}" + ], + "description": "If-Else Statement" + }, + "New Statement": { + "prefix": "new", + "body": [ + "const ${1:name} = new ${2:type}(${3:arguments});$0" + ], + "description": "New Statement" + }, + "Switch Statement": { + "prefix": "switch", + "body": [ + "switch (${1:key}) {", + "\tcase ${2:value}:", + "\t\t$0", + "\t\tbreak;", + "", + "\tdefault:", + "\t\tbreak;", + "}" + ], + "description": "Switch Statement" + }, + "While Statement": { + "prefix": "while", + "body": [ + "while (${1:condition}) {", + "\t$TM_SELECTED_TEXT$0", + "}" + ], + "description": "While Statement" + }, + "Do-While Statement": { + "prefix": "dowhile", + "body": [ + "do {", + "\t$TM_SELECTED_TEXT$0", + "} while (${1:condition});" + ], + "description": "Do-While Statement" + }, + "Try-Catch Statement": { + "prefix": "trycatch", + "body": [ + "try {", + "\t$TM_SELECTED_TEXT$0", + "} catch (${1:error}) {", + "\t", + "}" + ], + "description": "Try-Catch Statement" + }, + "Set Timeout Function": { + "prefix": "settimeout", + "body": [ + "setTimeout(() => {", + "\t$TM_SELECTED_TEXT$0", + "}, ${1:timeout});" + ], + "description": "Set Timeout Function" + }, + "Set Interval Function": { + "prefix": "setinterval", + "body": [ + "setInterval(() => {", + "\t$TM_SELECTED_TEXT$0", + "}, ${1:interval});" + ], + "description": "Set Interval Function" + }, + "Import external module.": { + "prefix": "import statement", + "body": [ + "import { $0 } from \"${1:module}\";" + ], + "description": "Import external module." + }, + "Region Start": { + "prefix": "#region", + "body": [ + "//#region $0" + ], + "description": "Folding Region Start" + }, + "Region End": { + "prefix": "#endregion", + "body": [ + "//#endregion" + ], + "description": "Folding Region End" + }, + "Log to the console": { + "prefix": "log", + "body": [ + "console.log($1);" + ], + "description": "Log to the console" + }, + "Log warning to console": { + "prefix": "warn", + "body": [ + "console.warn($1);" + ], + "description": "Log warning to the console" + }, + "Log error to console": { + "prefix": "error", + "body": [ + "console.error($1);" + ], + "description": "Log error to the console" + } +} diff --git a/src/index.ts b/src/index.ts index cdd0936..113d965 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,5 @@ -import { commands, ExtensionContext, services, workspace } from 'coc.nvim' +import { ExtensionContext, services } from 'coc.nvim' import TsserverService from './server' -import { AutoFixCommand, Command, ConfigurePluginCommand, FileReferencesCommand, OpenTsServerLogCommand, ReloadProjectsCommand, TypeScriptGoToProjectConfigCommand } from './server/commands' -import { OrganizeImportsCommand } from './server/organizeImports' import { PluginManager } from './utils/plugins' interface API { @@ -9,39 +7,10 @@ interface API { } export async function activate(context: ExtensionContext): Promise { - let { subscriptions, logger } = context - const config = workspace.getConfiguration().get('tsserver', {}) - if (!config.enable) return + let { subscriptions } = context const pluginManager = new PluginManager() - const service = new TsserverService(pluginManager) - function registCommand(cmd: Command): void { - let { id, execute } = cmd - subscriptions.push(commands.registerCommand(id as string, execute, cmd)) - } - registCommand(new ConfigurePluginCommand(pluginManager)) - registCommand(new AutoFixCommand(service)) - registCommand(new ReloadProjectsCommand(service)) - registCommand(new FileReferencesCommand(service)) - registCommand(new OpenTsServerLogCommand(service)) - registCommand(new TypeScriptGoToProjectConfigCommand(service)) - registCommand(new OrganizeImportsCommand(service)) - registCommand({ - id: 'tsserver.restart', - execute: (): void => { - // tslint:disable-next-line:no-floating-promises - service.stop().then(() => { - setTimeout(() => { - service.restart() - }, 100) - }) - } - }) - - service.start().then(() => { - subscriptions.push(services.regist(service)) - }, e => { - logger.error(`Error on service start:`, e) - }) + const service = new TsserverService(pluginManager, context.subscriptions) + subscriptions.push(services.regist(service)) return { configurePlugin: (pluginId: string, configuration: {}): void => { diff --git a/src/server/features/baseCodeLensProvider.ts b/src/server/features/baseCodeLensProvider.ts index 74299d7..ff47b04 100644 --- a/src/server/features/baseCodeLensProvider.ts +++ b/src/server/features/baseCodeLensProvider.ts @@ -46,7 +46,8 @@ export abstract class TypeScriptBaseCodeLensProvider implements CodeLensProvider public constructor( protected client: ITypeScriptServiceClient, - private cachedResponse: CachedNavTreeResponse + private cachedResponse: CachedNavTreeResponse, + protected modeId: string ) {} public get onDidChangeCodeLenses(): Event { @@ -116,38 +117,31 @@ export abstract class TypeScriptBaseCodeLensProvider implements CodeLensProvider ) } } - protected getSymbolRange( - document: TextDocument, - item: Proto.NavigationTree - ): Range | null { - if (!item) { - return null - } +} - // TS 3.0+ provides a span for just the symbol - if ((item as any).nameSpan) { - return typeConverters.Range.fromTextSpan((item as any).nameSpan) - } +export function getSymbolRange( + document: TextDocument, + item: Proto.NavigationTree +): Range | null { + if (item.nameSpan) { + return typeConverters.Range.fromTextSpan(item.nameSpan) + } - // In older versions, we have to calculate this manually. See #23924 - const span = item.spans && item.spans[0] - if (!span) { - return null - } + // In older versions, we have to calculate this manually. See #23924 + const span = item.spans && item.spans[0] + if (!span) { + return null + } - const range = typeConverters.Range.fromTextSpan(span) - const text = document.getText(range) + const range = typeConverters.Range.fromTextSpan(span) + const text = document.getText(range) - const identifierMatch = new RegExp( - `^(.*?(\\b|\\W))${escapeRegExp(item.text || '')}(\\b|\\W)`, - 'gm' - ) - const match = identifierMatch.exec(text) - const prefixLength = match ? match.index + match[1].length : 0 - const startOffset = document.offsetAt(range.start) + prefixLength - return { - start: document.positionAt(startOffset), - end: document.positionAt(startOffset + item.text.length) - } + const identifierMatch = new RegExp(`^(.*?(\\b|\\W))${escapeRegExp(item.text || '')}(\\b|\\W)`, 'gm') + const match = identifierMatch.exec(text) + const prefixLength = match ? match.index + match[1].length : 0 + const startOffset = document.offsetAt(range.start) + prefixLength + return { + start: document.positionAt(startOffset), + end: document.positionAt(startOffset + item.text.length) } } diff --git a/src/server/features/bufferSyncSupport.ts b/src/server/features/bufferSyncSupport.ts index 627108d..a7ae050 100644 --- a/src/server/features/bufferSyncSupport.ts +++ b/src/server/features/bufferSyncSupport.ts @@ -345,9 +345,7 @@ export default class BufferSyncSupport { } public listen(): void { - if (this.listening) { - return - } + if (this.listening) return this.listening = true workspace.onDidOpenTextDocument( this.openTextDocument, diff --git a/src/server/features/completionItemProvider.ts b/src/server/features/completionItemProvider.ts index 7f2ad21..8280ee2 100644 --- a/src/server/features/completionItemProvider.ts +++ b/src/server/features/completionItemProvider.ts @@ -251,7 +251,7 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP } const detail = details[0] if (!item.detail && detail.displayParts.length) { - item.detail = Previewer.plain(detail.displayParts) + item.detail = Previewer.plainWithLinks(detail.displayParts) } item.documentation = this.getDocumentation(detail) const { command, additionalTextEdits } = this.getCodeActions(detail, filepath) @@ -259,7 +259,7 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP item.additionalTextEdits = additionalTextEdits if (detail && item.insertTextFormat == InsertTextFormat.Snippet) { const shouldCompleteFunction = await this.isValidFunctionCompletionContext(filepath, position, token) - if (shouldCompleteFunction) { + if (shouldCompleteFunction && !item.insertText) { this.createSnippetOfFunctionCall(item, detail) } } @@ -354,12 +354,12 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP private getDocumentation(detail: Proto.CompletionEntryDetails): MarkupContent | undefined { let documentation = '' if (detail.source) { - const importPath = `'${Previewer.plain(detail.source)}'` + const importPath = `'${Previewer.plainWithLinks(detail.source)}'` const autoImportLabel = `Auto import from ${importPath}` documentation += `${autoImportLabel}\n` } let parts = [ - Previewer.plain(detail.documentation), + Previewer.plainWithLinks(detail.documentation), Previewer.tagsMarkdownPreview(detail.tags) ] parts = parts.filter(s => s && s.trim() != '') diff --git a/src/server/features/documentSymbol.ts b/src/server/features/documentSymbol.ts index 98a4586..3a78abc 100644 --- a/src/server/features/documentSymbol.ts +++ b/src/server/features/documentSymbol.ts @@ -38,6 +38,7 @@ const getSymbolKind = (kind: string): SymbolKind => { return SymbolKind.Variable case PConst.Kind.constructSignature: case PConst.Kind.constructorImplementation: + return SymbolKind.Constructor case PConst.Kind.function: case PConst.Kind.localFunction: return SymbolKind.Function diff --git a/src/server/features/fileConfigurationManager.ts b/src/server/features/fileConfigurationManager.ts index d465cff..f6674a6 100644 --- a/src/server/features/fileConfigurationManager.ts +++ b/src/server/features/fileConfigurationManager.ts @@ -40,6 +40,9 @@ export interface SuggestOptions { readonly importStatementSuggestions: boolean readonly includeCompletionsForImportStatements: boolean readonly includeCompletionsWithSnippetText: boolean + readonly includeCompletionsWithClassMemberSnippets: boolean + readonly generateReturnInDocTemplate: boolean + readonly includeCompletionsWithObjectLiteralMethodSnippets: boolean } export default class FileConfigurationManager { @@ -85,7 +88,13 @@ export default class FileConfigurationManager { } public async ensureConfigurationForDocument(document: TextDocument, token: CancellationToken): Promise { - let opts = await workspace.getFormatOptions(document.uri) + let opts: { insertSpaces: boolean, tabSize: number } + let cached = this.cachedMap.get(document.uri) + if (cached) { + opts = { insertSpaces: cached.formatOptions.convertTabsToSpaces, tabSize: cached.formatOptions.tabSize } + } else { + opts = await workspace.getFormatOptions(document.uri) + } return this.ensureConfigurationOptions(document, opts.insertSpaces, opts.tabSize, token) } @@ -159,9 +168,12 @@ export default class FileConfigurationManager { paths: config.get('paths', true), completeFunctionCalls: config.get('completeFunctionCalls', true), autoImports: config.get('autoImports', true), + includeCompletionsWithObjectLiteralMethodSnippets: config.get('suggest.objectLiteralMethodSnippets.enabled', true), + generateReturnInDocTemplate: config.get('jsdoc.generateReturns', true), importStatementSuggestions: config.get('importStatements', true), includeCompletionsForImportStatements: config.get('includeCompletionsForImportStatements', true), includeCompletionsWithSnippetText: config.get('includeCompletionsWithSnippetText', true), + includeCompletionsWithClassMemberSnippets: config.get('classMemberSnippets.enabled', true), includeAutomaticOptionalChainCompletions: config.get('includeAutomaticOptionalChainCompletions', true) } } @@ -170,17 +182,31 @@ export default class FileConfigurationManager { if (this.client.apiVersion.lt(API.v290)) { return {} } - const config = workspace.getConfiguration(`${language}.preferences`, uri) + const config = workspace.getConfiguration(language, uri) + const preferencesConfig = workspace.getConfiguration(`${language}.preferences`, uri) + const suggestConfig = this.getCompleteOptions(language) // getImportModuleSpecifierEndingPreference available on ts 2.9.0 - const preferences: Proto.UserPreferences & { importModuleSpecifierEnding?: string } = { - quotePreference: this.getQuoteStyle(config), - importModuleSpecifierPreference: getImportModuleSpecifier(config) as any, - importModuleSpecifierEnding: getImportModuleSpecifierEndingPreference(config), + const preferences: Proto.UserPreferences = { + quotePreference: this.getQuoteStyle(preferencesConfig), + importModuleSpecifierPreference: getImportModuleSpecifier(preferencesConfig) as any, + importModuleSpecifierEnding: getImportModuleSpecifierEndingPreference(preferencesConfig), + jsxAttributeCompletionStyle: getJsxAttributeCompletionStyle(preferencesConfig), allowTextChangesInNewFiles: uri.startsWith('file:'), allowRenameOfImportPath: true, - providePrefixAndSuffixTextForRename: config.get('renameShorthandProperties', true) === false ? false : config.get('useAliasesForRenames', true), - includeCompletionsForImportStatements: this.getCompleteOptions(language).includeCompletionsForImportStatements, - includeCompletionsWithSnippetText: this.getCompleteOptions(language).includeCompletionsWithSnippetText, + // can't support it with coc.nvim by now. + provideRefactorNotApplicableReason: false, + providePrefixAndSuffixTextForRename: preferencesConfig.get('renameShorthandProperties', true) === false ? false : preferencesConfig.get('useAliasesForRenames', true), + generateReturnInDocTemplate: suggestConfig.generateReturnInDocTemplate, + includeCompletionsForImportStatements: suggestConfig.includeCompletionsForImportStatements, + includeCompletionsWithClassMemberSnippets: suggestConfig.includeCompletionsWithClassMemberSnippets, + includeCompletionsWithSnippetText: suggestConfig.includeCompletionsWithSnippetText, + // @ts-expect-error until 4.7 + includeCompletionsWithObjectLiteralMethodSnippets: suggestConfig.includeCompletionsWithObjectLiteralMethodSnippets, + includeAutomaticOptionalChainCompletions: suggestConfig.includeAutomaticOptionalChainCompletions, + useLabelDetailsInCompletionEntries: true, + allowIncompleteCompletions: true, + displayPartsForJSDoc: true, + ...getInlayHintsPreferences(config), } return preferences } @@ -220,3 +246,41 @@ function getImportModuleSpecifierEndingPreference(config: WorkspaceConfiguration default: return 'auto' } } + +function getJsxAttributeCompletionStyle(config: WorkspaceConfiguration) { + switch (config.get('jsxAttributeCompletionStyle')) { + case 'braces': return 'braces' + case 'none': return 'none' + default: return 'auto' + } +} + +export class InlayHintSettingNames { + static readonly parameterNamesSuppressWhenArgumentMatchesName = 'inlayHints.parameterNames.suppressWhenArgumentMatchesName' + static readonly parameterNamesEnabled = 'inlayHints.parameterTypes.enabled' + static readonly variableTypesEnabled = 'inlayHints.variableTypes.enabled' + static readonly propertyDeclarationTypesEnabled = 'inlayHints.propertyDeclarationTypes.enabled' + static readonly functionLikeReturnTypesEnabled = 'inlayHints.functionLikeReturnTypes.enabled' + static readonly enumMemberValuesEnabled = 'inlayHints.enumMemberValues.enabled' +} + +export function getInlayHintsPreferences(config: WorkspaceConfiguration) { + return { + includeInlayParameterNameHints: getInlayParameterNameHintsPreference(config), + includeInlayParameterNameHintsWhenArgumentMatchesName: !config.get(InlayHintSettingNames.parameterNamesSuppressWhenArgumentMatchesName, true), + includeInlayFunctionParameterTypeHints: config.get(InlayHintSettingNames.parameterNamesEnabled, false), + includeInlayVariableTypeHints: config.get(InlayHintSettingNames.variableTypesEnabled, false), + includeInlayPropertyDeclarationTypeHints: config.get(InlayHintSettingNames.propertyDeclarationTypesEnabled, false), + includeInlayFunctionLikeReturnTypeHints: config.get(InlayHintSettingNames.functionLikeReturnTypesEnabled, false), + includeInlayEnumMemberValueHints: config.get(InlayHintSettingNames.enumMemberValuesEnabled, false), + } as const +} + +function getInlayParameterNameHintsPreference(config: WorkspaceConfiguration) { + switch (config.get('inlayHints.parameterNames.enabled')) { + case 'none': return 'none' + case 'literals': return 'literals' + case 'all': return 'all' + default: return undefined + } +} diff --git a/src/server/features/folding.ts b/src/server/features/folding.ts index 0917c41..1f257bf 100644 --- a/src/server/features/folding.ts +++ b/src/server/features/folding.ts @@ -45,16 +45,22 @@ export default class TypeScriptFoldingProvider implements FoldingRangeProvider { ): FoldingRange | undefined { const range = typeConverters.Range.fromTextSpan(span.textSpan) const kind = TypeScriptFoldingProvider.getFoldingRangeKind(span) + let { start, end } = range // Workaround for #49904 if (span.kind === 'comment') { let doc = workspace.getDocument(document.uri) - const line = doc.getline(range.start.line) + const line = doc.getline(start.line) if (line.match(/\/\/\s*#endregion/gi)) { return undefined } + } else if (span.kind === 'code') { + let doc = workspace.getDocument(document.uri) + if (end.line > start.line && /^\s*}/.test(doc.getline(end.line))) { + end.line -= 1 + end.character = doc.getline(end.line).length + } } - let { start, end } = range return FoldingRange.create(start.line, end.line, start.character, end.character, kind) } diff --git a/src/server/features/hover.ts b/src/server/features/hover.ts index a9d3e26..c669f44 100644 --- a/src/server/features/hover.ts +++ b/src/server/features/hover.ts @@ -7,7 +7,7 @@ import { HoverProvider } from 'coc.nvim' import { CancellationToken, Hover, MarkedString, Position } from 'vscode-languageserver-protocol' import * as Proto from '../protocol' import { ITypeScriptServiceClient } from '../typescriptService' -import { tagsMarkdownPreview } from '../utils/previewer' +import { markdownDocumentation } from '../utils/previewer' import * as typeConverters from '../utils/typeConverters' export default class TypeScriptHoverProvider implements HoverProvider { @@ -42,14 +42,16 @@ export default class TypeScriptHoverProvider implements HoverProvider { } private static getContents(data: Proto.QuickInfoResponseBody): MarkedString[] { // tslint:disable-line - const parts = [] - + const parts: MarkedString[] = [] if (data.displayString) { + // const displayParts: string[] = [] parts.push({ language: 'typescript', value: data.displayString }) } - - const tags = tagsMarkdownPreview(data.tags) - parts.push(data.documentation + (tags ? '\n\n' + tags : '')) + const markup = markdownDocumentation(data.documentation, data.tags) + parts.push({ + language: 'markdown', + value: markup.value + }) return parts } } diff --git a/src/server/features/implementationsCodeLens.ts b/src/server/features/implementationsCodeLens.ts index 3e870c0..bb82ae9 100644 --- a/src/server/features/implementationsCodeLens.ts +++ b/src/server/features/implementationsCodeLens.ts @@ -7,7 +7,7 @@ import { TextDocument } from 'coc.nvim' import * as Proto from '../protocol' import * as PConst from '../protocol.const' import * as typeConverters from '../utils/typeConverters' -import { TypeScriptBaseCodeLensProvider } from './baseCodeLensProvider' +import { TypeScriptBaseCodeLensProvider, getSymbolRange } from './baseCodeLensProvider' export default class TypeScriptImplementationsCodeLensProvider extends TypeScriptBaseCodeLensProvider { public async resolveCodeLens( @@ -21,43 +21,39 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip filepath, codeLens.range.start ) - try { - const response = await this.client.execute('implementation', args, token, { lowPriority: true }) - if (response && response.type == 'response' && response.body) { - const locations = response.body - .map(reference => { - return { - uri: this.client.toResource(reference.file), - range: { - start: typeConverters.Position.fromLocation(reference.start), - end: { - line: reference.start.line, - character: 0 - } - } - } - }) - // Exclude original from implementations - .filter( - location => !( - location.uri.toString() === uri && - location.range.start.line === codeLens.range.start.line && - location.range.start.character === - codeLens.range.start.character - ) - ) - - codeLens.command = this.getCommand(locations, codeLens) - return codeLens + const response = await this.client.execute('implementation', args, token, { lowPriority: true }) + if (response.type !== 'response' || !response.body) { + codeLens.command = { + title: response.type === 'cancelled' + ? 'cancelled' + : 'could not determine implementation', + command: '' } - } catch { - // noop - } - - codeLens.command = { - title: '0 implementations', - command: '' + return codeLens } + const locations = response.body + .map(reference => { + return { + uri: this.client.toResource(reference.file), + range: { + start: typeConverters.Position.fromLocation(reference.start), + end: { + line: reference.start.line, + character: 0 + } + } + } + }) + // Exclude original from implementations + .filter( + location => !( + location.uri.toString() === uri && + location.range.start.line === codeLens.range.start.line && + location.range.start.character === + codeLens.range.start.character + ) + ) + codeLens.command = this.getCommand(locations, codeLens) return codeLens } @@ -84,7 +80,7 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip ): Range | null { switch (item.kind) { case PConst.Kind.interface: - return super.getSymbolRange(document, item) + return getSymbolRange(document, item) case PConst.Kind.class: case PConst.Kind.method: @@ -92,7 +88,7 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip case PConst.Kind.memberGetAccessor: case PConst.Kind.memberSetAccessor: if (item.kindModifiers.match(/\babstract\b/g)) { - return super.getSymbolRange(document, item) + return getSymbolRange(document, item) } break } diff --git a/src/server/features/inlayHints.ts b/src/server/features/inlayHints.ts new file mode 100644 index 0000000..4ab268e --- /dev/null +++ b/src/server/features/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 { CancellationToken, Disposable, disposeAll, Emitter, Event, InlayHint, InlayHintKind, InlayHintsProvider, Range, TextDocument, workspace } from 'coc.nvim' +import type * as Proto from '../protocol' +import { ITypeScriptServiceClient } from '../typescriptService' +import API from '../utils/api' +import { LanguageDescription } from '../utils/languageDescription' +import * as typeConverters from '../utils/typeConverters' +import FileConfigurationManager, { getInlayHintsPreferences } from './fileConfigurationManager' + +export default class TypeScriptInlayHintsProvider implements InlayHintsProvider { + public static readonly minVersion = API.v440 + private disposables: Disposable[] = [] + private readonly _onDidChangeInlayHints = new Emitter() + public readonly onDidChangeInlayHints: Event = this._onDidChangeInlayHints.event + + constructor( + private readonly language: LanguageDescription, + private readonly client: ITypeScriptServiceClient, + private readonly fileConfigurationManager: FileConfigurationManager, + ) { + let section = `${language.id}.inlayHints` + workspace.onDidChangeConfiguration(async e => { + if (e.affectsConfiguration(section)) { + this._onDidChangeInlayHints.fire() + } + }, null, this.disposables) + // When a JS/TS file changes, change inlay hints for all visible editors + // since changes in one file can effect the hints the others. + workspace.onDidChangeTextDocument(e => { + let doc = workspace.getDocument(e.textDocument.uri) + if (language.languageIds.includes(doc.languageId)) { + this._onDidChangeInlayHints.fire() + } + }, null, this.disposables) + } + + public dispose(): void { + this._onDidChangeInlayHints.dispose() + disposeAll(this.disposables) + } + + async provideInlayHints(document: TextDocument, range: Range, token: CancellationToken): Promise { + const filepath = this.client.toOpenedFilePath(document.uri) + if (!filepath) return [] + + if (!areInlayHintsEnabledForFile(this.language, document)) { + return [] + } + const start = document.offsetAt(range.start) + const length = document.offsetAt(range.end) - start + await this.fileConfigurationManager.ensureConfigurationForDocument(document, token) + const response = await this.client.execute('provideInlayHints', { file: filepath, start, length }, token) + if (response.type !== 'response' || !response.success || !response.body) { + return [] + } + + return response.body.map(hint => { + return { + label: hint.text, + position: typeConverters.Position.fromLocation(hint.position), + kind: fromProtocolInlayHintKind(hint.kind), + paddingLeft: hint.whitespaceBefore, + paddingRight: hint.whitespaceAfter, + } + }) + } +} + +function fromProtocolInlayHintKind(kind: Proto.InlayHintKind): InlayHintKind { + switch (kind) { + case 'Parameter': return 2 + case 'Type': return 1 + case 'Enum': return undefined + default: return undefined + } +} + +function areInlayHintsEnabledForFile(language: LanguageDescription, document: TextDocument) { + const config = workspace.getConfiguration(language.id, document.uri) + const preferences = getInlayHintsPreferences(config) + return preferences.includeInlayParameterNameHints === 'literals' || + preferences.includeInlayParameterNameHints === 'all' || + preferences.includeInlayEnumMemberValueHints || + preferences.includeInlayFunctionLikeReturnTypeHints || + preferences.includeInlayFunctionParameterTypeHints || + preferences.includeInlayPropertyDeclarationTypeHints || + preferences.includeInlayVariableTypeHints +} diff --git a/src/server/features/jsDocCompletion.ts b/src/server/features/jsDocCompletion.ts new file mode 100644 index 0000000..22872aa --- /dev/null +++ b/src/server/features/jsDocCompletion.ts @@ -0,0 +1,121 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken, CompletionItem, CompletionItemKind, CompletionItemProvider, InsertTextFormat, Position, Range, SnippetString, TextDocument, workspace } from 'coc.nvim' +import { ITypeScriptServiceClient } from '../typescriptService' +import { LanguageDescription } from '../utils/languageDescription' +import * as typeConverters from '../utils/typeConverters' +import FileConfigurationManager from './fileConfigurationManager' + +const defaultJsDoc = new SnippetString(`/**\n * $0\n */`) + +function createCompleteItem(document: TextDocument, position: Position): CompletionItem { + const line = document.lineAt(position.line).text + const prefix = line.slice(0, position.character).match(/\/\**\s*$/) + const suffix = line.slice(position.character).match(/^\s*\**\//) + const start = Position.create(position.line, prefix ? position.character - prefix[0].length : position.character) + const range = Range.create(start, Position.create(start.line, start.character + (suffix ? suffix[0].length : 0))) + let insert = `/** */` + return { + label: insert, + kind: CompletionItemKind.Text, + insertTextFormat: InsertTextFormat.Snippet, + detail: 'JSDoc comment', + sortText: `\0`, + textEdit: { + newText: insert, + range + } + } +} + +export class JsDocCompletionProvider implements CompletionItemProvider { + constructor( + private readonly client: ITypeScriptServiceClient, + private readonly language: LanguageDescription, + private readonly fileConfigurationManager: FileConfigurationManager, + ) {} + + public async provideCompletionItems( + document: TextDocument, + position: Position, + token: CancellationToken + ): Promise { + if (!workspace.getConfiguration(this.language.id, document.uri).get('suggest.completeJSDocs')) { + return undefined + } + + const file = this.client.toOpenedFilePath(document.uri) + if (!file) { + return undefined + } + + if (!this.isPotentiallyValidDocCompletionPosition(document, position)) { + return undefined + } + + const response = await this.client.interruptGetErr(async () => { + await this.fileConfigurationManager.ensureConfigurationForDocument(document, token) + const args = typeConverters.Position.toFileLocationRequestArgs(file, position) + return this.client.execute('docCommentTemplate', args, token) + }) + if (response.type !== 'response' || !response.body) { + return undefined + } + + const item = createCompleteItem(document, position) + + // Workaround for #43619 + // docCommentTemplate previously returned undefined for empty jsdoc templates. + // TS 2.7 now returns a single line doc comment, which breaks indentation. + if (response.body.newText === '/** */') { + item.textEdit.newText = defaultJsDoc.value + } else { + item.textEdit.newText = templateToSnippet(response.body.newText).value + } + + return [item] + } + + private isPotentiallyValidDocCompletionPosition( + document: TextDocument, + position: Position + ): boolean { + // Only show the JSdoc completion when the everything before the cursor is whitespace + // or could be the opening of a comment + const line = document.lineAt(position.line).text + const prefix = line.slice(0, position.character) + if (!/^\s*$|\/\*\s*$|^\s*\/\*+\s*$/.test(prefix)) { + return false + } + + // And everything after is possibly a closing comment or more whitespace + const suffix = line.slice(position.character) + return /^\s*(\*+\/)?\s*$/.test(suffix) + } +} + +export function templateToSnippet(template: string): SnippetString { + // TODO: use append placeholder + let snippetIndex = 1 + template = template.replace(/\*\s$/gm, '*') + template = template.replace(/\$/g, '\\$') + template = template.replace(/^[ \t]*(?=(\/|[ ]\*))/gm, '') + template = template.replace(/^(\/\*\*\s*\*[ ]*)$/m, (x) => x + `\$0`) + template = template.replace(/\* @param([ ]\{\S+\})?\s+(\S+)[ \t]*$/gm, (_param, type, post) => { + let out = '* @param ' + if (type === ' {any}' || type === ' {*}') { + out += `{\$\{${snippetIndex++}:*\}} ` + } else if (type) { + out += type + ' ' + } + out += post + ` \${${snippetIndex++}}` + return out + }) + + template = template.replace(/\* @returns[ \t]*$/gm, `* @returns \${${snippetIndex++}}`) + + return new SnippetString(template) +} diff --git a/src/server/features/refactor.ts b/src/server/features/refactor.ts index 9a912f1..6c9fc4f 100644 --- a/src/server/features/refactor.ts +++ b/src/server/features/refactor.ts @@ -1,8 +1,8 @@ -import { CodeActionProvider, CodeActionProviderMetadata, commands, TextDocument, window, workspace } from 'coc.nvim' /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CodeActionProvider, Uri, CodeActionProviderMetadata, commands, TextDocument, window, workspace } from 'coc.nvim' import { CancellationToken, CodeAction, CodeActionContext, CodeActionKind, Range, WorkspaceEdit } from 'vscode-languageserver-protocol' import { Command, registCommand } from '../commands' import Proto from '../protocol' @@ -55,13 +55,22 @@ class ApplyRefactoringCommand implements Command { } private async toWorkspaceEdit(body: Proto.RefactorEditInfo): Promise { - for (const edit of body.edits) { - await workspace.createFile(edit.fileName, { ignoreIfExists: true }) - } let workspaceEdit = typeConverters.WorkspaceEdit.fromFileCodeEdits( this.client, body.edits ) + let documentChanges = workspaceEdit.documentChanges = workspaceEdit.documentChanges || [] + for (const edit of body.edits) { + let resource = this.client.toResource(edit.fileName) + if (Uri.parse(resource).scheme === 'file') { + // should create file first. + documentChanges.unshift({ + kind: 'create', + uri: resource, + options: { ignoreIfExists: true } + }) + } + } return workspaceEdit } } diff --git a/src/server/features/referencesCodeLens.ts b/src/server/features/referencesCodeLens.ts index 5c31854..4d33193 100644 --- a/src/server/features/referencesCodeLens.ts +++ b/src/server/features/referencesCodeLens.ts @@ -2,15 +2,16 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken, CodeLens, Range } from 'vscode-languageserver-protocol' -import { TextDocument } from 'coc.nvim' +import { CancellationToken, Position, Range } from 'vscode-languageserver-protocol' +import { TextDocument, workspace, CodeLens } from 'coc.nvim' +import { ExecutionTarget } from '../typescriptService' import * as Proto from '../protocol' import * as PConst from '../protocol.const' import * as typeConverters from '../utils/typeConverters' -import { TypeScriptBaseCodeLensProvider } from './baseCodeLensProvider' +import { TypeScriptBaseCodeLensProvider, getSymbolRange } from './baseCodeLensProvider' export default class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLensProvider { - public resolveCodeLens( + public async resolveCodeLens( codeLens: CodeLens, token: CancellationToken ): Promise { @@ -20,47 +21,35 @@ export default class TypeScriptReferencesCodeLensProvider extends TypeScriptBase filepath, codeLens.range.start ) - return this.client - .execute('references', args, token, { - lowPriority: true - }) - .then(response => { - if (!response || response.type != 'response' || !response.body) { - throw codeLens - } + let response = await this.client.execute('references', args, token, { + lowPriority: true, + executionTarget: ExecutionTarget.Semantic + }) + if (!response || response.type != 'response' || !response.body) { + codeLens.command = { + title: response.type === 'cancelled' + ? 'cancelled' + : 'could not determine references', + command: '' + } + return codeLens + } - const locations = response.body.refs - .map(reference => - typeConverters.Location.fromTextSpan( - this.client.toResource(reference.file), - reference - ) - ) - .filter( - location => - // Exclude original definition from references - !( - location.uri.toString() === uri && - location.range.start.line === codeLens.range.start.line && - location.range.start.character === - codeLens.range.start.character - ) - ) + const locations = response.body.refs + .filter(reference => !reference.isDefinition) + .map(reference => + typeConverters.Location.fromTextSpan( + this.client.toResource(reference.file), + reference + ) + ) - codeLens.command = { - title: locations.length === 1 ? '1 reference' : `${locations.length} references`, - command: locations.length ? 'editor.action.showReferences' : '', - arguments: [uri, codeLens.range.start, locations] - } - return codeLens - }) - .catch(() => { - codeLens.command = { - title: '0 references', - command: '' - } - return codeLens - }) + codeLens.command = { + title: locations.length === 1 ? '1 reference' : `${locations.length} references`, + command: locations.length ? 'editor.action.showReferences' : '', + arguments: [uri, codeLens.range.start, locations] + } + return codeLens } protected extractSymbol( @@ -69,37 +58,68 @@ export default class TypeScriptReferencesCodeLensProvider extends TypeScriptBase parent: Proto.NavigationTree | null ): Range | null { if (parent && parent.kind === PConst.Kind.enum) { - return super.getSymbolRange(document, item) + return getSymbolRange(document, item) } switch (item.kind) { + case PConst.Kind.function: { + const showOnAllFunctions = workspace.getConfiguration(this.modeId).get('referencesCodeLens.showOnAllFunctions') + if (showOnAllFunctions) { + return getSymbolRange(document, item) + } + } + // fallthrough + case PConst.Kind.const: case PConst.Kind.let: case PConst.Kind.variable: - case PConst.Kind.function: // Only show references for exported variables - if (!item.kindModifiers.match(/\bexport\b/)) { - break + if (/\bexport\b/.test(item.kindModifiers)) { + return getSymbolRange(document, item) } - // fallthrough + break case PConst.Kind.class: if (item.text === '') { break } - // fallthrough + return getSymbolRange(document, item) - case PConst.Kind.method: - case PConst.Kind.memberVariable: - case PConst.Kind.memberGetAccessor: - case PConst.Kind.memberSetAccessor: - case PConst.Kind.constructorImplementation: case PConst.Kind.interface: case PConst.Kind.type: case PConst.Kind.enum: - return super.getSymbolRange(document, item) + return getSymbolRange(document, item) + + case PConst.Kind.method: + case PConst.Kind.memberGetAccessor: + case PConst.Kind.memberSetAccessor: + case PConst.Kind.constructorImplementation: + case PConst.Kind.memberVariable: + // Don't show if child and parent have same start + // For https://github.com/microsoft/vscode/issues/90396 + if (parent && + comparePosition(typeConverters.Position.fromLocation(parent.spans[0].start), typeConverters.Position.fromLocation(item.spans[0].start)) == 0 + ) { + return null + } + + // Only show if parent is a class type object (not a literal) + switch (parent?.kind) { + case PConst.Kind.class: + case PConst.Kind.interface: + case PConst.Kind.type: + return getSymbolRange(document, item) + } + break } return null } } + +export function comparePosition(position: Position, other: Position): number { + if (position.line > other.line) return 1 + if (other.line == position.line && position.character > other.character) return 1 + if (other.line == position.line && position.character == other.character) return 0 + return -1 +} diff --git a/src/server/features/rename.ts b/src/server/features/rename.ts index 8df0966..cdb04dd 100644 --- a/src/server/features/rename.ts +++ b/src/server/features/rename.ts @@ -16,7 +16,7 @@ export default class TypeScriptRenameProvider implements RenameProvider { public constructor( private readonly client: ITypeScriptServiceClient, private readonly fileConfigurationManager: FileConfigurationManager - ) { } + ) {} public async prepareRename( document: TextDocument, @@ -60,8 +60,8 @@ export default class TypeScriptRenameProvider implements RenameProvider { } if (this.client.apiVersion.gte(API.v310)) { - if ((renameInfo as any).fileToRename) { - const edits = await this.renameFile((renameInfo as any).fileToRename, newName, token) + if (renameInfo.fileToRename) { + const edits = await this.renameFile(renameInfo.fileToRename, newName, token) if (edits) { return edits } else { diff --git a/src/server/features/semanticTokens.ts b/src/server/features/semanticTokens.ts new file mode 100644 index 0000000..2ad3e49 --- /dev/null +++ b/src/server/features/semanticTokens.ts @@ -0,0 +1,294 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, Range, SemanticTokens, SemanticTokensBuilder, TextDocument, workspace } from 'coc.nvim' +import { SemanticTokensLegend } from 'vscode-languageserver-protocol' +import * as Proto from '../protocol' +import { ExecConfig, ITypeScriptServiceClient, ServerResponse } from '../typescriptService' +import API from '../utils/api' + +// as we don't do deltas, for performance reasons, don't compute semantic tokens for documents above that limit +const CONTENT_LENGTH_LIMIT = 100000 + +/** + * Prototype of a DocumentSemanticTokensProvider, relying on the experimental `encodedSemanticClassifications-full` request from the TypeScript server. + * As the results retured by the TypeScript server are limited, we also add a Typescript plugin (typescript-vscode-sh-plugin) to enrich the returned token. + * See https://github.com/aeschli/typescript-vscode-sh-plugin. + */ +export default class TypeScriptDocumentSemanticTokensProvider implements DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider { + public static readonly minVersion = API.v370 + + constructor(private readonly client: ITypeScriptServiceClient) {} + + getLegend(): SemanticTokensLegend { + return { + tokenTypes, + tokenModifiers + } + } + + private logIgnored(uri: string): void { + this.client.logger.warn(`${uri} content length exceed limit 100000`) + } + + async provideDocumentSemanticTokens(document: TextDocument, token: CancellationToken): Promise { + const file = this.client.toOpenedFilePath(document.uri) + if (!file || document.getText().length > CONTENT_LENGTH_LIMIT) { + this.logIgnored(document.uri) + return null + } + return this._provideSemanticTokens(document, { file, start: 0, length: document.getText().length }, token) + } + + async provideDocumentRangeSemanticTokens(document: TextDocument, range: Range, token: CancellationToken): Promise { + const file = this.client.toOpenedFilePath(document.uri) + if (!file || (document.offsetAt(range.end) - document.offsetAt(range.start) > CONTENT_LENGTH_LIMIT)) { + this.logIgnored(document.uri) + return null + } + + const start = document.offsetAt(range.start) + const length = document.offsetAt(range.end) - start + return this._provideSemanticTokens(document, { file, start, length }, token) + } + + async _provideSemanticTokens(document: TextDocument, requestArg: Proto.EncodedSemanticClassificationsRequestArgs, token: CancellationToken): Promise { + const file = this.client.toOpenedFilePath(document.uri) + if (!file) { + return null + } + + const versionBeforeRequest = document.version + + requestArg.format = '2020' + + const response = await (this.client as ExperimentalProtocol.IExtendedTypeScriptServiceClient).execute('encodedSemanticClassifications-full', requestArg, token, { + cancelOnResourceChange: document.uri + }) + if (response.type !== 'response' || !response.body) { + return null + } + + const versionAfterRequest = document.version + if (versionBeforeRequest !== versionAfterRequest) { + // cannot convert result's offsets to (linecol) values correctly + // a new request will come in soon... + // + // here we cannot return null, because returning null would remove all semantic tokens. + // we must throw to indicate that the semantic tokens should not be removed. + // using the string busy here because it is not logged to error telemetry if the error text contains busy. + + // as the new request will come in right after our response, we first wait for the document activity to stop + await waitForDocumentChangesToEnd(document) + + throw new Error('Canceled') + } + + const doc = workspace.getDocument(document.uri) + const tokenSpan = response.body.spans + + const builder = new SemanticTokensBuilder() + let i = 0 + while (i < tokenSpan.length) { + const offset = tokenSpan[i++] + const length = tokenSpan[i++] + const tsClassification = tokenSpan[i++] + + let tokenModifiers = 0 + let tokenType = getTokenTypeFromClassification(tsClassification) + if (tokenType !== undefined) { + // it's a classification as returned by the typescript-vscode-sh-plugin + tokenModifiers = getTokenModifierFromClassification(tsClassification) + } else { + // typescript-vscode-sh-plugin is not present + tokenType = tokenTypeMap[tsClassification] + if (tokenType === undefined) { + continue + } + } + + // we can use the document's range conversion methods because the result is at the same version as the document + const startPos = document.positionAt(offset) + const endPos = document.positionAt(offset + length) + for (let line = startPos.line; line <= endPos.line; line++) { + const startCharacter = (line === startPos.line ? startPos.character : 0) + const endCharacter = (line === endPos.line ? endPos.character : doc.getline(line).length) + builder.push(line, startCharacter, endCharacter - startCharacter, tokenType, tokenModifiers) + } + } + return builder.build() + } +} + +function waitForDocumentChangesToEnd(document: TextDocument) { + let version = document.version + return new Promise((s) => { + const iv = setInterval(_ => { + if (document.version === version) { + clearInterval(iv) + s() + } + version = document.version + }, 400) + }) +} + +function getTokenTypeFromClassification(tsClassification: number): number | undefined { + if (tsClassification > TokenEncodingConsts.modifierMask) { + return (tsClassification >> TokenEncodingConsts.typeOffset) - 1 + } + return undefined +} + +function getTokenModifierFromClassification(tsClassification: number) { + return tsClassification & TokenEncodingConsts.modifierMask +} + +// typescript encodes type and modifiers in the classification: +// TSClassification = (TokenType + 1) << 8 + TokenModifier + +const enum TokenType { + class = 0, + enum = 1, + interface = 2, + namespace = 3, + typeParameter = 4, + type = 5, + parameter = 6, + variable = 7, + enumMember = 8, + property = 9, + function = 10, + method = 11, + _ = 12 +} +const enum TokenModifier { + declaration = 0, + static = 1, + async = 2, + readonly = 3, + defaultLibrary = 4, + local = 5, + _ = 6 +} +const enum TokenEncodingConsts { + typeOffset = 8, + modifierMask = 255 +} + +const tokenTypes: string[] = [] +tokenTypes[TokenType.class] = 'class' +tokenTypes[TokenType.enum] = 'enum' +tokenTypes[TokenType.interface] = 'interface' +tokenTypes[TokenType.namespace] = 'namespace' +tokenTypes[TokenType.typeParameter] = 'typeParameter' +tokenTypes[TokenType.type] = 'type' +tokenTypes[TokenType.parameter] = 'parameter' +tokenTypes[TokenType.variable] = 'variable' +tokenTypes[TokenType.enumMember] = 'enumMember' +tokenTypes[TokenType.property] = 'property' +tokenTypes[TokenType.function] = 'function' +tokenTypes[TokenType.method] = 'method' + +const tokenModifiers: string[] = [] +tokenModifiers[TokenModifier.async] = 'async' +tokenModifiers[TokenModifier.declaration] = 'declaration' +tokenModifiers[TokenModifier.readonly] = 'readonly' +tokenModifiers[TokenModifier.static] = 'static' +tokenModifiers[TokenModifier.local] = 'local' +tokenModifiers[TokenModifier.defaultLibrary] = 'defaultLibrary' + +export namespace ExperimentalProtocol { + + export interface IExtendedTypeScriptServiceClient { + execute( + command: K, + args: ExperimentalProtocol.ExtendedTsServerRequests[K][0], + token: CancellationToken, + config?: ExecConfig + ): Promise> + } + + /** + * A request to get encoded semantic classifications for a span in the file + */ + export interface EncodedSemanticClassificationsRequest extends Proto.FileRequest { + arguments: EncodedSemanticClassificationsRequestArgs + } + + /** + * Arguments for EncodedSemanticClassificationsRequest request. + */ + export interface EncodedSemanticClassificationsRequestArgs extends Proto.FileRequestArgs { + /** + * Start position of the span. + */ + start: number + /** + * Length of the span. + */ + length: number + } + + export const enum EndOfLineState { + None, + InMultiLineCommentTrivia, + InSingleQuoteStringLiteral, + InDoubleQuoteStringLiteral, + InTemplateHeadOrNoSubstitutionTemplate, + InTemplateMiddleOrTail, + InTemplateSubstitutionPosition, + } + + export const enum ClassificationType { + comment = 1, + identifier = 2, + keyword = 3, + numericLiteral = 4, + operator = 5, + stringLiteral = 6, + regularExpressionLiteral = 7, + whiteSpace = 8, + text = 9, + punctuation = 10, + className = 11, + enumName = 12, + interfaceName = 13, + moduleName = 14, + typeParameterName = 15, + typeAliasName = 16, + parameterName = 17, + docCommentTagName = 18, + jsxOpenTagName = 19, + jsxCloseTagName = 20, + jsxSelfClosingTagName = 21, + jsxAttribute = 22, + jsxText = 23, + jsxAttributeStringLiteralValue = 24, + bigintLiteral = 25, + } + + export interface EncodedSemanticClassificationsResponse extends Proto.Response { + body?: { + endOfLineState: EndOfLineState + spans: number[] + } + } + + export interface ExtendedTsServerRequests { + 'encodedSemanticClassifications-full': [ExperimentalProtocol.EncodedSemanticClassificationsRequestArgs, ExperimentalProtocol.EncodedSemanticClassificationsResponse] + } +} + +// mapping for the original ExperimentalProtocol.ClassificationType from TypeScript (only used when plugin is not available) +const tokenTypeMap: number[] = [] +tokenTypeMap[ExperimentalProtocol.ClassificationType.className] = TokenType.class +tokenTypeMap[ExperimentalProtocol.ClassificationType.enumName] = TokenType.enum +tokenTypeMap[ExperimentalProtocol.ClassificationType.interfaceName] = TokenType.interface +tokenTypeMap[ExperimentalProtocol.ClassificationType.moduleName] = TokenType.namespace +tokenTypeMap[ExperimentalProtocol.ClassificationType.typeParameterName] = TokenType.typeParameter +tokenTypeMap[ExperimentalProtocol.ClassificationType.typeAliasName] = TokenType.type +tokenTypeMap[ExperimentalProtocol.ClassificationType.parameterName] = TokenType.parameter + diff --git a/src/server/features/signatureHelp.ts b/src/server/features/signatureHelp.ts index c0a48d0..2a74a89 100644 --- a/src/server/features/signatureHelp.ts +++ b/src/server/features/signatureHelp.ts @@ -60,13 +60,13 @@ export default class TypeScriptSignatureHelpProvider implements SignatureHelpPro private convertSignature(item: Proto.SignatureHelpItem): SignatureInformation { let parameters = item.parameters.map(p => { return { - label: Previewer.plain(p.displayParts), + label: Previewer.plainWithLinks(p.displayParts), documentation: Previewer.markdownDocumentation(p.documentation, []) } }) - let label = Previewer.plain(item.prefixDisplayParts) - label += parameters.map(parameter => parameter.label).join(Previewer.plain(item.separatorDisplayParts)) - label += Previewer.plain(item.suffixDisplayParts) + let label = Previewer.plainWithLinks(item.prefixDisplayParts) + label += parameters.map(parameter => parameter.label).join(Previewer.plainWithLinks(item.separatorDisplayParts)) + label += Previewer.plainWithLinks(item.suffixDisplayParts) return { label, documentation: Previewer.markdownDocumentation( diff --git a/src/server/features/tagClosing.ts b/src/server/features/tagClosing.ts index bb53810..b11ffb4 100644 --- a/src/server/features/tagClosing.ts +++ b/src/server/features/tagClosing.ts @@ -5,13 +5,10 @@ import { CancellationTokenSource, Disposable, disposeAll, Position, Range, snippetManager, events, workspace, InsertChange } from 'coc.nvim' import * as Proto from '../protocol' import { ITypeScriptServiceClient } from '../typescriptService' -import API from '../utils/api' import SnippetString from '../utils/SnippetString' import * as typeConverters from '../utils/typeConverters' export default class TagClosing implements Disposable { - public static readonly minVersion = API.v300 - private static _configurationLanguages: Record = { 'javascriptreact': 'javascript', 'typescriptreact': 'typescript', @@ -21,27 +18,21 @@ export default class TagClosing implements Disposable { private _disposed = false private _timeout: NodeJS.Timer | undefined = undefined private _cancel: CancellationTokenSource | undefined = undefined - private lastInsert: string constructor( private readonly client: ITypeScriptServiceClient, private readonly descriptionLanguageId: string ) { - events.on('InsertCharPre', character => { - this.lastInsert = character - }, null, this._disposables) - events.on('TextChangedI', this.onChange, this, this._disposables) - events.on('TextChangedP', this.onChange, this, this._disposables) + events.on('TextInsert', this.onInsertChange, this, this._disposables) } - private async onChange(bufnr: number, change: InsertChange): Promise { + private async onInsertChange(bufnr: number, change: InsertChange, lastInsert: string): Promise { let doc = workspace.getDocument((bufnr)) if (!doc || !doc.attached) return let enabled = this.isEnabled(doc.filetype, doc.uri) if (!enabled) return let { pre, changedtick, lnum } = change - if (!pre.endsWith('/') && !pre.endsWith('>')) return - if (!pre.endsWith(this.lastInsert)) return + if (lastInsert !== '/' && lastInsert != '>') return if (pre.length > 1 && pre[pre.length - 2] == '>') return const filepath = this.client.toOpenedFilePath(doc.uri) if (!filepath) return @@ -73,7 +64,6 @@ export default class TagClosing implements Disposable { return } if (this._disposed) return - const insertion = response.body if (doc.changedtick === changedtick) { snippetManager.insertSnippet( @@ -82,7 +72,7 @@ export default class TagClosing implements Disposable { Range.create(position, position) ) } - }, 50) + }, 30) } private isEnabled(languageId: string, uri: string): boolean { diff --git a/src/server/features/watchBuild.ts b/src/server/features/watchBuild.ts index ea40c7b..7ea12c3 100644 --- a/src/server/features/watchBuild.ts +++ b/src/server/features/watchBuild.ts @@ -1,24 +1,10 @@ -import { commands, disposeAll, StatusBarItem, TaskOptions, Uri, window, workspace } from 'coc.nvim' +import { commands, Disposable, disposeAll, StatusBarItem, TaskOptions, Uri, window, workspace } from 'coc.nvim' import path from 'path' -import { Disposable, Location } from 'vscode-languageserver-protocol' import TypeScriptServiceClient from '../typescriptServiceClient' const countRegex = /Found\s+(\d+)\s+error/ const errorRegex = /^(.+)\((\d+),(\d+)\):\s(\w+)\sTS(\d+):\s*(.+)$/ -interface ErrorItem { - location: Location - text: string - type: string -} - -enum TscStatus { - INIT, - COMPILING, - RUNNING, - ERROR, -} - export default class WatchProject implements Disposable { private disposables: Disposable[] = [] public static readonly id: string = 'tsserver.watchBuild' @@ -119,7 +105,7 @@ export default class WatchProject implements Disposable { return } - const tsconfigPath = workspace.getConfiguration('tsserver').get('tsconfigPath', 'tsconfig.json'); + const tsconfigPath = workspace.getConfiguration('tsserver').get('tsconfigPath', 'tsconfig.json') let find = await workspace.findUp([tsconfigPath]) if (!find) { window.showMessage(`${tsconfigPath} not found!`, 'error') diff --git a/src/server/index.ts b/src/server/index.ts index 92a0b1a..2d42456 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,6 +1,8 @@ -import { disposeAll, IServiceProvider, ServiceStat, workspace, WorkspaceConfiguration } from 'coc.nvim' +import { commands, disposeAll, IServiceProvider, ServiceStat, workspace, WorkspaceConfiguration } from 'coc.nvim' import { Disposable, DocumentSelector, Emitter, Event } from 'vscode-languageserver-protocol' import { PluginManager } from '../utils/plugins' +import { AutoFixCommand, Command, ConfigurePluginCommand, FileReferencesCommand, OpenTsServerLogCommand, ReloadProjectsCommand, TypeScriptGoToProjectConfigCommand } from './commands' +import { OrganizeImportsCommand, SourceImportsCommand } from './organizeImports' import TypeScriptServiceClientHost from './typescriptServiceClientHost' import { LanguageDescription, standardLanguageDescriptions } from './utils/languageDescription' @@ -10,23 +12,69 @@ export default class TsserverService implements IServiceProvider { public enable: boolean // supported language types public selector: DocumentSelector - public state = ServiceStat.Initial + public _state = ServiceStat.Initial public clientHost: TypeScriptServiceClientHost private _onDidServiceReady = new Emitter() public readonly onServiceReady: Event = this._onDidServiceReady.event private readonly disposables: Disposable[] = [] private descriptions: LanguageDescription[] = [] - constructor(private pluginManager: PluginManager) { + constructor(private pluginManager: PluginManager, private readonly subscriptions: Disposable[]) { const config = workspace.getConfiguration('tsserver') - const enableJavascript = !!config.get('enableJavascript') + const enableJavascript = config.get('enableJavascript', true) this.enable = config.get('enable') this.descriptions = standardLanguageDescriptions.filter(o => { return enableJavascript ? true : o.id != 'javascript' }) + workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('tsserver')) { + const config = workspace.getConfiguration('tsserver') + let enable = this.enable + this.enable = config.get('enable', true) + if (enable !== this.enable) { + if (this.enable) { + void this.start() + } else { + void this.stop() + } + } + } + }) this.selector = this.descriptions.reduce((arr, c) => { - return arr.concat(c.modeIds) + return arr.concat(c.languageIds) }, []) + this.registCommands() + } + + // public state = ServiceStat.Initial + + public get state(): ServiceStat { + if (this.clientHost) { + return this.clientHost.serviceClient.state + } + return this._state + } + + private registCommands(): void { + let { subscriptions } = this + const registCommand = (cmd: Command): void => { + let { id, execute } = cmd + subscriptions.push(commands.registerCommand(id as string, execute, cmd)) + } + registCommand(new ConfigurePluginCommand(this.pluginManager)) + registCommand(new AutoFixCommand(this)) + registCommand(new ReloadProjectsCommand(this)) + registCommand(new FileReferencesCommand(this)) + registCommand(new OpenTsServerLogCommand(this)) + registCommand(new TypeScriptGoToProjectConfigCommand(this)) + registCommand(new OrganizeImportsCommand(this)) + registCommand(new SourceImportsCommand(this)) + registCommand({ + id: 'tsserver.restart', + execute: (): void => { + this.restart() + } + }) } public get config(): WorkspaceConfiguration { @@ -51,44 +99,43 @@ export default class TsserverService implements IServiceProvider { }) } - public start(): Promise { - if (this.clientHost) return - this.state = ServiceStat.Starting - this.clientHost = new TypeScriptServiceClientHost(this.descriptions, this.pluginManager) - this.disposables.push(this.clientHost) + public async start(): Promise { + if (!this.enable || this._state == ServiceStat.Starting) return + this._state = ServiceStat.Starting + if (this.clientHost) { + let client = this.clientHost.serviceClient + client.restartTsServer() + return + } + let tscPath = await workspace.nvim.getVar('Tsserver_path') as string | null + this.clientHost = new TypeScriptServiceClientHost(this.descriptions, this.pluginManager, tscPath) let client = this.clientHost.serviceClient - return new Promise(resolve => { - let started = false - client.onTsServerStarted(() => { - Object.defineProperty(this, 'state', { - get: () => { - return this.clientHost.serviceClient.state - } - }) + await new Promise(resolve => { + client.onReady(() => { this._onDidServiceReady.fire(void 0) - if (!started) { - started = true - resolve() - } + resolve(undefined) }) }) } - public dispose(): void { - disposeAll(this.disposables) - } - public async restart(): Promise { - if (!this.clientHost) return - let client = this.clientHost.serviceClient - await client.restartTsServer() + if (!this.enable) return + await this.stop() + await this.start() } public async stop(): Promise { if (!this.clientHost) return - this.clientHost.reset() let client = this.clientHost.serviceClient await client.stop() - return + this.clientHost?.dispose() + this.clientHost = null + this._state = ServiceStat.Stopped + } + + public dispose(): void { + void this.stop() + this._onDidServiceReady.dispose() + disposeAll(this.disposables) } } diff --git a/src/server/languageProvider.ts b/src/server/languageProvider.ts index 660cc4a..78b9c2b 100644 --- a/src/server/languageProvider.ts +++ b/src/server/languageProvider.ts @@ -19,16 +19,19 @@ import FormattingProvider from './features/formatting' import HoverProvider from './features/hover' import ImplementationsCodeLensProvider from './features/implementationsCodeLens' import ImportfixProvider from './features/importFix' +import TypeScriptInlayHintsProvider from './features/inlayHints' import InstallModuleProvider from './features/moduleInstall' import QuickfixProvider from './features/quickfix' import RefactorProvider from './features/refactor' import ReferenceProvider from './features/references' import ReferencesCodeLensProvider from './features/referencesCodeLens' import RenameProvider from './features/rename' +import SemanticTokensProvider from './features/semanticTokens' import SignatureHelpProvider from './features/signatureHelp' import SmartSelection from './features/smartSelect' import TagClosing from './features/tagClosing' import UpdateImportsOnFileRenameHandler from './features/updatePathOnRename' +import { JsDocCompletionProvider } from './features/jsDocCompletion' import { OrganizeImportsCodeActionProvider } from './organizeImports' import TypeScriptServiceClient from './typescriptServiceClient' import API from './utils/api' @@ -49,13 +52,8 @@ export default class LanguageProvider { ) { workspace.onDidChangeConfiguration(this.configurationChanged, this, this.disposables) this.configurationChanged() - - let initialized = false - client.onTsServerStarted(async () => { // tslint:disable-line - if (!initialized) { - initialized = true - this.registerProviders(client, typingsStatus) - } + client.onReady(() => { + this.registerProviders(client, typingsStatus) }) } @@ -77,14 +75,20 @@ export default class LanguageProvider { client: TypeScriptServiceClient, typingsStatus: TypingsStatus ): void { - let languageIds = this.description.modeIds - let clientId = `tsserver-${this.description.id}` + let languageIds = this.description.languageIds + let clientId = `tsc-${this.description.id}` this._register( languages.registerCompletionItemProvider(clientId, 'TSC', languageIds, new CompletionItemProvider(client, typingsStatus, this.fileConfigurationManager, this.description.id), CompletionItemProvider.triggerCharacters ) ) + this._register( + languages.registerCompletionItemProvider(`tsc-${this.description.id}-jsdoc`, 'TSC', languageIds, + new JsDocCompletionProvider(client, this.description, this.fileConfigurationManager), + ['*', ' '] + ) + ) if (this.client.apiVersion.gte(API.v230)) { this._register(languages.registerCompletionItemProvider( `${this.description.id}-directive`, @@ -107,9 +111,18 @@ export default class LanguageProvider { this._register(languages.registerDocumentRangeFormatProvider(languageIds, formatProvider)) this._register(languages.registerOnTypeFormattingEditProvider(languageIds, formatProvider, [';', '}', '\n', String.fromCharCode(27)])) this._register(languages.registerCodeActionProvider(languageIds, new InstallModuleProvider(client), 'tsserver')) - if (typeof languages['registerCallHierarchyProvider'] === 'function') { + if (this.client.apiVersion.gte(API.v380) && typeof languages['registerCallHierarchyProvider'] === 'function') { this._register(languages.registerCallHierarchyProvider(languageIds, new CallHierarchyProvider(client))) } + if (this.client.apiVersion.gte(API.v370)) { + const provider = new SemanticTokensProvider(client) + if (typeof languages['registerDocumentSemanticTokensProvider'] === 'function') { + this._register(languages.registerDocumentSemanticTokensProvider(languageIds, provider, provider.getLegend())) + } + if (typeof languages['registerDocumentRangeSemanticTokensProvider'] === 'function') { + this._register(languages.registerDocumentRangeSemanticTokensProvider(languageIds, provider, provider.getLegend())) + } + } let { fileConfigurationManager } = this let conf = fileConfigurationManager.getLanguageConfiguration(this.id) @@ -145,10 +158,10 @@ export default class LanguageProvider { 'tsserver', [CodeActionKind.QuickFix])) let cachedResponse = new CachedNavTreeResponse() if (this.client.apiVersion.gte(API.v206) && conf.get('referencesCodeLens.enable')) { - this._register(languages.registerCodeLensProvider(languageIds, new ReferencesCodeLensProvider(client, cachedResponse))) + this._register(languages.registerCodeLensProvider(languageIds, new ReferencesCodeLensProvider(client, cachedResponse, this.description.id))) } if (this.client.apiVersion.gte(API.v220) && conf.get('implementationsCodeLens.enable')) { - this._register(languages.registerCodeLensProvider(languageIds, new ImplementationsCodeLensProvider(client, cachedResponse))) + this._register(languages.registerCodeLensProvider(languageIds, new ImplementationsCodeLensProvider(client, cachedResponse, this.description.id))) } if (this.client.apiVersion.gte(API.v350)) { this._register(languages.registerSelectionRangeProvider(languageIds, new SmartSelection(this.client))) @@ -156,16 +169,34 @@ export default class LanguageProvider { if (this.client.apiVersion.gte(API.v300)) { this._register(new TagClosing(this.client, this.description.id)) } + if (this.client.apiVersion.gte(API.v440)) { + if (typeof languages.registerInlayHintsProvider === 'function') { + let provider = new TypeScriptInlayHintsProvider(this.description, this.client, this.fileConfigurationManager) + this._register(provider) + this._register(languages.registerInlayHintsProvider(languageIds, provider)) + } else { + this.client.logger.error(`languages.registerInlayHintsProvider is not a function, inlay hints won't work`) + } + } } public handles(resource: string, doc: TextDocument): boolean { - if (doc && this.description.modeIds.indexOf(doc.languageId) >= 0) { + if (doc && this.description.languageIds.includes(doc.languageId)) { return true } - const base = path.basename(Uri.parse(resource).fsPath) + return this.handlesConfigFile(Uri.parse(resource)) + } + + private handlesConfigFile(uri: Uri): boolean { + const base = path.basename(uri.fsPath) return !!base && (!!this.description.configFilePattern && this.description.configFilePattern.test(base)) } + public handlesUri(resource: Uri): boolean { + const ext = path.extname(resource.path).slice(1).toLowerCase() + return this.description.standardFileExtensions.includes(ext) || this.handlesConfigFile(resource) + } + private get id(): string { // tslint:disable-line return this.description.id } diff --git a/src/server/organizeImports.ts b/src/server/organizeImports.ts index 4cca6c7..c5d4ff7 100644 --- a/src/server/organizeImports.ts +++ b/src/server/organizeImports.ts @@ -19,9 +19,10 @@ export class OrganizeImportsCommand implements Command { ) { } - private async _execute(client: TypeScriptServiceClient, document: TextDocument): Promise { + private async _execute(client: TypeScriptServiceClient, document: TextDocument, sortOnly = false): Promise { let file = client.toPath(document.uri) const args: Proto.OrganizeImportsRequestArgs = { + skipDestructiveCodeActions: sortOnly, scope: { type: 'file', args: { @@ -38,7 +39,7 @@ export class OrganizeImportsCommand implements Command { client, response.body ) - let keys = Object.keys(edit.changes) + let keys = Object.keys(edit.changes || {}) if (keys.length == 1) { let doc = workspace.getDocument(keys[0]) if (doc) { @@ -49,7 +50,7 @@ export class OrganizeImportsCommand implements Command { if (edit) await workspace.applyEdit(edit) } - public async execute(document?: TextDocument): Promise { + public async execute(document?: TextDocument, sortOnly = false): Promise { let client = await this.service.getClientHost() if (!document) { let doc = await workspace.document @@ -61,10 +62,14 @@ export class OrganizeImportsCommand implements Command { } document = doc.textDocument } - await this._execute(client.serviceClient, document) + await this._execute(client.serviceClient, document, sortOnly) } } +export class SourceImportsCommand extends OrganizeImportsCommand { + public readonly id = 'tsserver.sortImports' +} + export class OrganizeImportsCodeActionProvider implements CodeActionProvider { // public static readonly minVersion = API.v280 @@ -91,11 +96,16 @@ export class OrganizeImportsCodeActionProvider implements CodeActionProvider { } await this.fileConfigManager.ensureConfigurationForDocument(document, token) - const action = CodeAction.create('Organize Imports', { + const organizeImportsAction = CodeAction.create('Organize Imports', { title: '', command: 'tsserver.organizeImports', arguments: [document] }, CodeActionKind.SourceOrganizeImports) - return [action] + const sortImportsAction = CodeAction.create('Sort Imports', { + title: '', + command: 'tsserver.sortImports', + arguments: [document, true] + }, 'source.sortImports') + return [organizeImportsAction, sortImportsAction] } } diff --git a/src/server/tsServerProcess.ts b/src/server/tsServerProcess.ts new file mode 100644 index 0000000..5877237 --- /dev/null +++ b/src/server/tsServerProcess.ts @@ -0,0 +1,55 @@ + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import cp from 'child_process' +import { Disposable } from 'vscode-languageserver-protocol' +import * as Proto from './protocol' +import { Reader } from './utils/wireProtocol' + +export interface ToCancelOnResourceChanged { + readonly resource: string + cancel(): void +} + +export default class ForkedTsServerProcess implements Disposable { + private readonly _reader: Reader + + constructor(private childProcess: cp.ChildProcess) { + this._reader = new Reader(this.childProcess.stdout) + } + + public readonly toCancelOnResourceChange = new Set() + + public onExit(cb: (err: any, signal: string) => void): void { + this.childProcess.on('exit', cb) + } + + public write(serverRequest: Proto.Request): void { + this.childProcess.stdin.write( + JSON.stringify(serverRequest) + '\r\n', + 'utf8' + ) + } + + public onData(handler: (data: Proto.Response) => void): void { + this._reader.onData(handler) + } + + public onError(handler: (err: Error) => void): void { + this.childProcess.on('error', handler) + this._reader.onError(handler) + } + + public kill(): void { + this.toCancelOnResourceChange.clear() + this.childProcess.kill() + this._reader.dispose() + } + + public dispose(): void { + this.toCancelOnResourceChange.clear() + this._reader.dispose() + } +} diff --git a/src/server/typescriptService.ts b/src/server/typescriptService.ts index 2b6f7b6..f576567 100644 --- a/src/server/typescriptService.ts +++ b/src/server/typescriptService.ts @@ -34,7 +34,7 @@ export interface TypeScriptServerPlugin { readonly languages: string[] } -export enum ExectuionTarget { +export enum ExecutionTarget { Semantic, Syntax } @@ -43,7 +43,7 @@ export type ExecConfig = { readonly lowPriority?: boolean readonly nonRecoverable?: boolean readonly cancelOnResourceChange?: string - readonly executionTarget?: ExectuionTarget + readonly executionTarget?: ExecutionTarget } export interface TypeScriptRequestTypes { @@ -84,6 +84,7 @@ export interface TypeScriptRequestTypes { 'provideCallHierarchyIncomingCalls': [Proto.FileLocationRequestArgs, Proto.ProvideCallHierarchyIncomingCallsResponse] 'provideCallHierarchyOutgoingCalls': [Proto.FileLocationRequestArgs, Proto.ProvideCallHierarchyOutgoingCallsResponse] 'fileReferences': [Proto.FileRequestArgs, Proto.FileReferencesResponse] + 'provideInlayHints': [Proto.InlayHintsRequestArgs, Proto.InlayHintsResponse] } export interface ITypeScriptServiceClient { diff --git a/src/server/typescriptServiceClient.ts b/src/server/typescriptServiceClient.ts index 301d7f4..5c6c58c 100644 --- a/src/server/typescriptServiceClient.ts +++ b/src/server/typescriptServiceClient.ts @@ -2,13 +2,12 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import cp from 'child_process' import { Document, ServiceStat, Uri, window, workspace } from 'coc.nvim' import fs from 'fs' import os from 'os' import path from 'path' import { CancellationToken, CancellationTokenSource, Disposable, Emitter, Event } from 'vscode-languageserver-protocol' -import * as fileSchemes from '../utils/fileSchemess' +import * as fileSchemes from '../utils/fileSchemes' import { PluginManager } from '../utils/plugins' import { CallbackMap } from './callbackMap' import BufferSyncSupport from './features/bufferSyncSupport' @@ -20,50 +19,12 @@ import { ExecConfig, ITypeScriptServiceClient, ServerResponse } from './typescri import API from './utils/api' import { TsServerLogLevel, TypeScriptServiceConfiguration } from './utils/configuration' import Logger from './utils/logger' -import { fork, getTempDirectory, getTempFile, IForkOptions, makeRandomHexString } from './utils/process' +import { fork, getTempDirectory, createTempDirectory, getTempFile, IForkOptions, makeRandomHexString } from './utils/process' import Tracer from './utils/tracer' import { inferredProjectConfig } from './utils/tsconfig' import { TypeScriptVersion, TypeScriptVersionProvider } from './utils/versionProvider' import VersionStatus from './utils/versionStatus' -import { ICallback, Reader } from './utils/wireProtocol' - -interface ToCancelOnResourceChanged { - readonly resource: string - cancel(): void -} - -class ForkedTsServerProcess { - constructor(private childProcess: cp.ChildProcess) {} - - public readonly toCancelOnResourceChange = new Set() - - public onError(cb: (err: Error) => void): void { - this.childProcess.on('error', cb) - } - - public onExit(cb: (err: any) => void): void { - this.childProcess.on('exit', cb) - } - - public write(serverRequest: Proto.Request): void { - this.childProcess.stdin.write( - JSON.stringify(serverRequest) + '\r\n', - 'utf8' - ) - } - - public createReader( - callback: ICallback, - onError: (error: any) => void - ): void { - // tslint:disable-next-line:no-unused-expression - new Reader(this.childProcess.stdout, callback, onError) - } - - public kill(): void { - this.childProcess.kill() - } -} +import ForkedTsServerProcess, { ToCancelOnResourceChanged } from './tsServerProcess' export interface TsDiagnostics { readonly kind: DiagnosticKind @@ -72,6 +33,8 @@ export interface TsDiagnostics { } export default class TypeScriptServiceClient implements ITypeScriptServiceClient { + private token: number = 0 + private noRestart = false public state = ServiceStat.Initial public readonly logger: Logger = new Logger() public readonly bufferSyncSupport: BufferSyncSupport @@ -84,14 +47,13 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient private versionProvider: TypeScriptVersionProvider private tsServerLogFile: string | null = null private tsServerProcess: ForkedTsServerProcess | undefined - private servicePromise: Thenable | null - private lastError: Error | null private lastStart: number private numberRestarts: number private cancellationPipeName: string | null = null private _callbacks = new CallbackMap() private _requestQueue = new RequestQueue() private _pendingResponses = new Set() + private _onReady?: { promise: Promise; resolve: () => void; reject: () => void } private versionStatus: VersionStatus private readonly _onTsServerStarted = new Emitter() @@ -108,13 +70,20 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient constructor( public readonly pluginManager: PluginManager, - public readonly modeIds: string[] + public readonly modeIds: string[], + private readonly tscPathVim: string | undefined ) { this.pathSeparator = path.sep this.lastStart = Date.now() - this.servicePromise = null - this.lastError = null this.numberRestarts = 0 + let resolve: () => void + let reject: () => void + const p = new Promise((res, rej) => { + resolve = res + reject = rej + }) + this._onReady = { promise: p, resolve: resolve!, reject: reject! } + this.fileConfigurationManager = new FileConfigurationManager(this) this._configuration = TypeScriptServiceConfiguration.loadFromWorkspace() this.versionProvider = new TypeScriptVersionProvider(this._configuration) @@ -130,7 +99,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient }, null, this.disposables) this.bufferSyncSupport = new BufferSyncSupport(this, modeIds) - this.onTsServerStarted(() => { + this.onReady(() => { this.bufferSyncSupport.listen() }) @@ -163,17 +132,20 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient return this._configuration } + public onReady(f: () => void): Promise { + return this._onReady!.promise.then(f) + } + public dispose(): void { - if (this.servicePromise) { - this.servicePromise - .then(childProcess => { - childProcess.kill() - }) - .then(undefined, () => void 0) - } + this.tsServerProcess.kill() + this.diagnosticsManager.dispose() this.bufferSyncSupport.dispose() this.logger.dispose() this._onTsServerStarted.dispose() + this._onProjectLanguageServiceStateChanged.dispose() + this._onDidBeginInstallTypings.dispose() + this._onDidEndInstallTypings.dispose() + this._onTypesInstallerInitializationFailed.dispose() this._onResendModelsRequested.dispose() this.versionStatus.dispose() } @@ -186,40 +158,29 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient this.logger.error(message, data) } - public restartTsServer(): Promise { - const start = () => { - this.servicePromise = this.startService(true) - return this.servicePromise - } - - if (this.servicePromise) { - return Promise.resolve(this.servicePromise.then(childProcess => { - this.state = ServiceStat.Stopping - this.info('Killing TS Server') - this.isRestarting = true - childProcess.kill() - this.servicePromise = null - }).then(start)) - } else { - return Promise.resolve(start()) + public restartTsServer(): void { + if (this.tsServerProcess) { + this.state = ServiceStat.Stopping + this.info('Killing TS Server') + this.isRestarting = true + this.tsServerProcess.kill() } + this.startService(true) } public stop(): Promise { - if (!this.servicePromise) return - return new Promise((resolve, reject) => { - this.servicePromise.then(childProcess => { - if (this.state == ServiceStat.Running) { - this.info('Killing TS Server') - childProcess.onExit(() => { - resolve() - }) - childProcess.kill() - this.servicePromise = null - } else { + return new Promise(resolve => { + let { tsServerProcess } = this + if (tsServerProcess && this.state == ServiceStat.Running) { + this.info('Killing TS Server') + tsServerProcess.onExit(() => { resolve() - } - }, reject) + }) + this.noRestart = true + tsServerProcess.kill() + } else { + resolve() + } }) } @@ -253,125 +214,101 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient return this._tscPath } - private service(): Thenable { - if (this.servicePromise) { - return this.servicePromise - } - if (this.lastError) { - return Promise.reject(this.lastError) - } - return this.startService().then(() => { - if (this.servicePromise) { - return this.servicePromise - } - }) - } - public ensureServiceStarted(): void { - if (!this.servicePromise) { - this.startService().catch(err => { - window.showMessage(`TSServer start failed: ${err.message}`, 'error') - this.error(`Service start failed: ${err.stack}`) - }) + if (!this.tsServerProcess) { + this.startService() } } - private async startService(resendModels = false): Promise { + private startService(resendModels = false): ForkedTsServerProcess | undefined { const { ignoreLocalTsserver } = this.configuration let currentVersion: TypeScriptVersion - if (!ignoreLocalTsserver) currentVersion = this.versionProvider.getLocalVersion() + if (this.tscPathVim) currentVersion = this.versionProvider.getVersionFromTscPath(this.tscPathVim) + if (!currentVersion && !ignoreLocalTsserver) currentVersion = this.versionProvider.getLocalVersion() if (!currentVersion || !fs.existsSync(currentVersion.tsServerPath)) { + this.info('Local tsserver not found, using bundled tsserver with coc-tsserver.') currentVersion = this.versionProvider.getDefaultVersion() } if (!currentVersion || !currentVersion.isValid) { if (this.configuration.globalTsdk) { - window.showMessage(`Can not find typescript module, in 'tsserver.tsdk': ${this.configuration.globalTsdk}`, 'error') + window.showErrorMessage(`Can not find typescript module, in 'tsserver.tsdk': ${this.configuration.globalTsdk}`) } else { - window.showMessage(`Can not find typescript module, run ':CocInstall coc-tsserver' to fix it!`, 'error') + window.showErrorMessage(`Can not find typescript module, run ':CocInstall coc-tsserver' to fix it!`) } return } this._apiVersion = currentVersion.version this._tscPath = currentVersion.tscPath + workspace.nvim.setVar('Tsserver_path', this._tscPath, true) this.versionStatus.onDidChangeTypeScriptVersion(currentVersion) - this.lastError = null - const tsServerForkArgs = await this.getTsServerArgs(currentVersion) - const debugPort = this._configuration.debugPort - const maxTsServerMemory = this._configuration.maxTsServerMemory - const options = { - execArgv: [ - ...(debugPort ? [`--inspect=${debugPort}`] : []), // [`--debug-brk=5859`] - ...(maxTsServerMemory ? [`--max-old-space-size=${maxTsServerMemory}`] : []), - ], - cwd: workspace.root - } - this.servicePromise = this.startProcess(currentVersion, tsServerForkArgs, options, resendModels) - return this.servicePromise + const tsServerForkArgs = this.getTsServerArgs(currentVersion) + const options = { execArgv: this.getExecArgv() } + return this.startProcess(currentVersion, tsServerForkArgs, options, resendModels) } - private startProcess(currentVersion: TypeScriptVersion, args: string[], options: IForkOptions, resendModels: boolean): Promise { + private getExecArgv(): string[] { + const args: string[] = [] + const debugPort = getDebugPort() + if (debugPort) { + const isBreak = process.env[process.env.remoteName ? 'TSS_REMOTE_DEBUG_BRK' : 'TSS_DEBUG_BRK'] !== undefined + const inspectFlag = isBreak ? '--inspect-brk' : '--inspect' + args.push(`${inspectFlag}=${debugPort}`) + } + const maxTsServerMemory = this._configuration.maxTsServerMemory + if (maxTsServerMemory) { + args.push(`--max-old-space-size=${maxTsServerMemory}`) + } + return args + } + + private startProcess(currentVersion: TypeScriptVersion, args: string[], options: IForkOptions, resendModels: boolean): ForkedTsServerProcess { + const myToken = ++this.token this.state = ServiceStat.Starting - return new Promise((resolve, reject) => { - try { - fork( - currentVersion.tsServerPath, - args, - options, - this.logger, - (err: any, childProcess: cp.ChildProcess | null) => { - if (err || !childProcess) { - this.state = ServiceStat.StartFailed - this.lastError = err - this.error('Starting TSServer failed with error.', err.stack) - return - } - this.state = ServiceStat.Running - this.info('Started TSServer', JSON.stringify(currentVersion, null, 2)) - const handle = new ForkedTsServerProcess(childProcess) - this.tsServerProcess = handle - this.lastStart = Date.now() - - handle.onError((err: Error) => { - this.lastError = err - this.error('TSServer errored with error.', err) - this.error(`TSServer log file: ${this.tsServerLogFile || ''}`) - window.showMessage(`TSServer errored with error. ${err.message}`, 'error') - this.serviceExited(false) - }) - handle.onExit((code: any) => { - if (code == null) { - this.info('TSServer normal exit') - } else { - this.error(`TSServer exited with code: ${code}`) - } - this.info(`TSServer log file: ${this.tsServerLogFile || ''}`) - this.serviceExited(!this.isRestarting) - this.isRestarting = false - }) - - handle.createReader( - msg => { - this.dispatchMessage(msg) - }, - error => { - this.error('ReaderError', error) - } - ) - resolve(handle) - this.serviceStarted(resendModels) - this._onTsServerStarted.fire(currentVersion.version) - } - ) - } catch (e) { - reject(e) - } - }) + try { + let childProcess = fork(currentVersion.tsServerPath, args, options, this.logger) + this.state = ServiceStat.Running + this.info('Starting TSServer', JSON.stringify(currentVersion, null, 2)) + const handle = new ForkedTsServerProcess(childProcess) + this.tsServerProcess = handle + this.lastStart = Date.now() + handle.onError((err: Error) => { + if (this.token != myToken) return + window.showErrorMessage(`TypeScript language server exited with error. Error message is: ${err.message}`) + this.error('TSServer errored with error.', err) + this.error(`TSServer log file: ${this.tsServerLogFile || ''}`) + window.showMessage(`TSServer errored with error. ${err.message}`, 'error') + this.serviceExited(false) + }) + handle.onExit((code: any, signal: string) => { + handle.dispose() + if (this.token != myToken) return + if (code == null) { + this.info(`TSServer exited. Signal: ${signal}`) + } else { + this.error(`TSServer exited with code: ${code}. Signal: ${signal}`) + } + this.info(`TSServer log file: ${this.tsServerLogFile || ''}`) + this.serviceExited(!this.isRestarting) + this.isRestarting = false + }) + handle.onData(msg => { + this.dispatchMessage(msg) + }) + this.serviceStarted(resendModels) + this._onReady!.resolve() + this._onTsServerStarted.fire(currentVersion.version) + return handle + } catch (err) { + this.state = ServiceStat.StartFailed + this.error('Starting TSServer failed with error.', err.stack) + return undefined + } } public async openTsServerLogFile(): Promise { const isRoot = process.getuid && process.getuid() == 0 let echoErr = (msg: string) => { - window.showMessage(msg, 'error') + window.showErrorMessage(msg) } if (isRoot) { echoErr('Log disabled for root user.') @@ -408,6 +345,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient preferences: { providePrefixAndSuffixTextForRename: true, allowRenameOfImportPath: true, + includePackageJsonAutoImports: this._configuration.includePackageJsonAutoImports }, watchOptions } @@ -448,12 +386,15 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient private serviceExited(restart: boolean): void { this.state = ServiceStat.Stopped - this.servicePromise = null this.tsServerLogFile = null this._callbacks.destroy('Service died.') this._callbacks = new CallbackMap() this._requestQueue = new RequestQueue() this._pendingResponses = new Set() + if (this.noRestart) { + this.noRestart = false + return + } if (restart) { const diff = Date.now() - this.lastStart this.numberRestarts++ @@ -470,7 +411,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient } } if (startService) { - this.startService(true) // tslint:disable-line + this.startService(true) } } } @@ -482,7 +423,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient public toOpenedFilePath(uri: string, options: { suppressAlertOnFailure?: boolean } = {}): string | undefined { if (!this.bufferSyncSupport.ensureHasBuffer(uri)) { if (!options.suppressAlertOnFailure) { - console.error(`Unexpected resource ${uri}`) + this.error(`Unexpected resource ${uri}`) } return undefined } @@ -490,6 +431,9 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient } public toResource(filepath: string): string { + if (filepath.includes('zipfile:')) { + return filepath.replace(/.*zipfile:/, 'zipfile://'); + } if (this._apiVersion.gte(API.v213)) { if (filepath.startsWith(this.inMemoryResourcePrefix + 'untitled:')) { let resource = Uri.parse(filepath) @@ -599,13 +543,14 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient } private fatalError(command: string, error: any): void { - console.error(`A non-recoverable error occured while executing tsserver command: ${command}`) - + this.error(`A non-recoverable error occured while executing tsserver command: ${command}`) if (this.state === ServiceStat.Running) { this.info('Killing TS Server by fatal error:', error) - this.service().then(service => { - service.kill() - }) + let { tsServerProcess } = this + if (tsServerProcess) { + this.tsServerProcess = undefined + tsServerProcess.kill() + } } } @@ -630,7 +575,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: CancellationToken, expectsResult: false, lowPriority?: boolean }): undefined private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise> private executeImpl(command: string, args: any, executeInfo: { isAsync: boolean, token?: CancellationToken, expectsResult: boolean, lowPriority?: boolean }): Promise> | undefined { - if (this.servicePromise == null) { + if (!this.tsServerProcess) { return Promise.resolve(undefined) } this.bufferSyncSupport.beforeCommand(command) @@ -678,16 +623,15 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient if (requestItem.expectsResponse && !requestItem.isAsync) { this._pendingResponses.add(requestItem.request.seq) } - this.service().then(childProcess => { - try { - childProcess.write(serverRequest) - } catch (err) { - const callback = this.fetchCallback(serverRequest.seq) - if (callback) { - callback.onError(err) - } + if (!this.tsServerProcess) return + try { + this.tsServerProcess.write(serverRequest) + } catch (err) { + const callback = this.fetchCallback(serverRequest.seq) + if (callback) { + callback.onError(err) } - }) + } } private tryCancelRequest(seq: number, command: string): boolean { @@ -722,7 +666,6 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient if (!callback) { return undefined } - this._pendingResponses.delete(seq) return callback } @@ -840,8 +783,9 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient } } - private async getTsServerArgs(currentVersion: TypeScriptVersion): Promise { + private getTsServerArgs(currentVersion: TypeScriptVersion): string[] { const args: string[] = [] + args.push('--allowLocalPluginLoads') if (this.apiVersion.gte(API.v250)) { @@ -859,10 +803,10 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient args.push('--cancellationPipeName', this.cancellationPipeName + '*') } + const logDir = getTempDirectory() if (this.apiVersion.gte(API.v222)) { const isRoot = process.getuid && process.getuid() == 0 if (this._configuration.tsServerLogLevel !== TsServerLogLevel.Off && !isRoot) { - const logDir = getTempDirectory() if (logDir) { this.tsServerLogFile = path.join(logDir, `tsserver.log`) this.info('TSServer log file :', this.tsServerLogFile) @@ -881,6 +825,16 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient } } + if (this._configuration.enableTsServerTracing) { + let tsServerTraceDirectory = createTempDirectory(`tsserver-trace-${makeRandomHexString(5)}`) + if (tsServerTraceDirectory) { + args.push('--traceDirectory', tsServerTraceDirectory) + this.info('TSServer trace directory :', tsServerTraceDirectory) + } else { + this.error('Could not create TSServer trace directory') + } + } + if (this.apiVersion.gte(API.v230)) { const pluginNames = this.pluginManager.plugins.map(x => x.name) let pluginPaths = this._configuration.tsServerPluginPaths @@ -954,11 +908,8 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient public configurePlugin(pluginName: string, configuration: {}): any { if (this.apiVersion.gte(API.v314)) { - if (!this.servicePromise) return - this.servicePromise.then(() => { - // tslint:disable-next-line: no-floating-promises - this.executeWithoutWaitingForResponse('configurePlugin', { pluginName, configuration }) - }) + if (!this.tsServerProcess) return + this.executeWithoutWaitingForResponse('configurePlugin', { pluginName, configuration }) } } @@ -1001,3 +952,15 @@ function getQueueingType( } return lowPriority ? RequestQueueingType.LowPriority : RequestQueueingType.Normal } + +function getDebugPort(): number | undefined { + let debugBrk = process.env[process.env.remoteName ? 'TSS_REMOTE_DEBUG_BRK' : 'TSS_DEBUG_BRK'] + let value = debugBrk || process.env[process.env.remoteName ? 'TSS_REMOTE_DEBUG_BRK' : 'TSS_DEBUG_BRK'] + if (value) { + const port = parseInt(value) + if (!isNaN(port)) { + return port + } + } + return undefined +} diff --git a/src/server/typescriptServiceClientHost.ts b/src/server/typescriptServiceClientHost.ts index dea0311..43e591f 100644 --- a/src/server/typescriptServiceClientHost.ts +++ b/src/server/typescriptServiceClientHost.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { disposeAll, languages, TextDocument, Uri, workspace } from 'coc.nvim' +import { ConfigurationChangeEvent, disposeAll, languages, TextDocument, Uri, workspace } from 'coc.nvim' import { CancellationToken, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, Disposable, Position, Range } from 'vscode-languageserver-protocol' import { flatten } from '../utils/arrays' import { PluginManager } from '../utils/plugins' @@ -22,6 +22,7 @@ import TypingsStatus, { AtaProgressReporter } from './utils/typingsStatus' const styleCheckDiagnostics = [ 6133, // variable is declared but never used 6138, // property is declared but its value is never read + 6192, // allImportsAreUnused 7027, // unreachable code detected 7028, // unused label 7029, // fall through case in switch @@ -37,7 +38,7 @@ export default class TypeScriptServiceClientHost implements Disposable { private readonly fileConfigurationManager: FileConfigurationManager private reportStyleCheckAsWarnings = true - constructor(descriptions: LanguageDescription[], pluginManager: PluginManager) { + constructor(descriptions: LanguageDescription[], pluginManager: PluginManager, tscPath: string | null) { let timer: NodeJS.Timer const handleProjectChange = () => { if (timer) clearTimeout(timer) @@ -56,7 +57,7 @@ export default class TypeScriptServiceClientHost implements Disposable { packageFileWatcher.onDidChange(handleProjectChange, this, this.disposables) const allModeIds = this.getAllModeIds(descriptions, pluginManager) - this.client = new TypeScriptServiceClient(pluginManager, allModeIds) + this.client = new TypeScriptServiceClient(pluginManager, allModeIds, tscPath) this.disposables.push(this.client) this.client.onDiagnosticsReceived(({ kind, resource, diagnostics }) => { this.diagnosticsReceived(kind, resource, diagnostics).catch(e => { @@ -102,39 +103,43 @@ export default class TypeScriptServiceClientHost implements Disposable { ) this.languagePerId.set(description.id, manager) } - const languageIds = new Set() - for (const plugin of pluginManager.plugins) { - if (plugin.configNamespace && plugin.languages.length) { + this.client.ensureServiceStarted() + this.client.onReady(() => { + const languageIds = new Set() + for (const plugin of pluginManager.plugins) { + if (plugin.configNamespace && plugin.languages.length) { + this.registerExtensionLanguageProvider({ + id: plugin.configNamespace, + languageIds: Array.from(plugin.languages), + diagnosticSource: 'ts-plugin', + diagnosticLanguage: DiagnosticLanguage.TypeScript, + diagnosticOwner: 'typescript', + standardFileExtensions: [], + isExternal: true + }) + } else { + for (const language of plugin.languages) { + languageIds.add(language) + } + } + } + + if (languageIds.size) { this.registerExtensionLanguageProvider({ - id: plugin.configNamespace, - modeIds: Array.from(plugin.languages), + id: 'typescript-plugins', + languageIds: Array.from(languageIds.values()), diagnosticSource: 'ts-plugin', diagnosticLanguage: DiagnosticLanguage.TypeScript, diagnosticOwner: 'typescript', + standardFileExtensions: [], isExternal: true }) - } else { - for (const language of plugin.languages) { - languageIds.add(language) - } } - } - - if (languageIds.size) { - this.registerExtensionLanguageProvider({ - id: 'typescript-plugins', - modeIds: Array.from(languageIds.values()), - diagnosticSource: 'ts-plugin', - diagnosticLanguage: DiagnosticLanguage.TypeScript, - diagnosticOwner: 'typescript', - isExternal: true - }) - } - - this.client.ensureServiceStarted() + }) this.client.onTsServerStarted(() => { this.triggerAllDiagnostics() }) + workspace.onDidChangeConfiguration(this.configurationChanged, this, this.disposables) this.configurationChanged() } @@ -155,10 +160,6 @@ export default class TypeScriptServiceClientHost implements Disposable { this.ataProgressReporter.dispose() } - public reset(): void { - this.fileConfigurationManager.reset() - } - public get serviceClient(): TypeScriptServiceClient { return this.client } @@ -174,17 +175,20 @@ export default class TypeScriptServiceClientHost implements Disposable { return this.languagePerId.get(languageId) } - private configurationChanged(): void { - const config = workspace.getConfiguration('tsserver') - this.reportStyleCheckAsWarnings = config.get('reportStyleChecksAsWarnings', true) + private configurationChanged(e?: ConfigurationChangeEvent): void { + if (!e || e.affectsConfiguration('tsserver')) { + const config = workspace.getConfiguration('tsserver') + this.reportStyleCheckAsWarnings = config.get('reportStyleChecksAsWarnings', true) + } } public async findLanguage(uri: string): Promise { try { let doc = this.client.getDocument(uri) - if (!doc) return undefined let languages = Array.from(this.languagePerId.values()) - return languages.find(language => language.handles(uri, doc.textDocument)) + // possible not opened + if (doc) return languages.find(language => language.handles(uri, doc.textDocument)) + return languages.find(language => language.handlesUri(Uri.parse(uri))) } catch { return undefined } @@ -289,7 +293,7 @@ export default class TypeScriptServiceClientHost implements Disposable { private getAllModeIds(descriptions: LanguageDescription[], pluginManager: PluginManager): string[] { const allModeIds = flatten([ - ...descriptions.map(x => x.modeIds), + ...descriptions.map(x => x.languageIds), ...pluginManager.plugins.map(x => x.languages) ]) return allModeIds diff --git a/src/server/utils/api.ts b/src/server/utils/api.ts index 6f6008a..0dce70a 100644 --- a/src/server/utils/api.ts +++ b/src/server/utils/api.ts @@ -35,6 +35,7 @@ export default class API { public static readonly v340 = API.fromSimpleString('3.4.0') public static readonly v345 = API.fromSimpleString('3.4.5') public static readonly v350 = API.fromSimpleString('3.5.0') + public static readonly v370 = API.fromSimpleString('3.7.0') public static readonly v380 = API.fromSimpleString('3.8.0') public static readonly v381 = API.fromSimpleString('3.8.1') public static readonly v390 = API.fromSimpleString('3.9.0') @@ -42,6 +43,7 @@ export default class API { public static readonly v401 = API.fromSimpleString('4.0.1') public static readonly v420 = API.fromSimpleString('4.2.0') public static readonly v430 = API.fromSimpleString('4.3.0') + public static readonly v440 = API.fromSimpleString('4.4.0') public static fromVersionString(versionString: string): API { let version = semver.valid(versionString) diff --git a/src/server/utils/completionItem.ts b/src/server/utils/completionItem.ts index 1ccfec2..4c2f920 100644 --- a/src/server/utils/completionItem.ts +++ b/src/server/utils/completionItem.ts @@ -59,6 +59,9 @@ export function convertCompletionEntry( insertText = label insertTextFormat = InsertTextFormat.Snippet } + if (tsEntry.isSnippet) { + insertTextFormat = InsertTextFormat.Snippet + } let textEdit: TextEdit | null = null if (tsEntry.replacementSpan) { @@ -73,7 +76,7 @@ export function convertCompletionEntry( if (tsEntry.kindModifiers) { const kindModifiers = new Set(tsEntry.kindModifiers.split(/,|\s+/g)) if (kindModifiers.has(PConst.KindModifiers.optional)) { - insertText = label + insertText = insertText ?? label label += '?' } @@ -136,6 +139,7 @@ function convertKind(kind: string): CompletionItemKind { case PConst.Kind.memberSetAccessor: return CompletionItemKind.Field case PConst.Kind.function: + case PConst.Kind.localFunction: return CompletionItemKind.Function case PConst.Kind.method: case PConst.Kind.constructSignature: diff --git a/src/server/utils/configuration.ts b/src/server/utils/configuration.ts index 7ee4c21..c320899 100644 --- a/src/server/utils/configuration.ts +++ b/src/server/utils/configuration.ts @@ -40,14 +40,25 @@ export namespace TsServerLogLevel { export class TypeScriptServiceConfiguration { private _configuration: WorkspaceConfiguration + private _includePackageJsonAutoImports: 'auto' | 'on' | 'off' private constructor() { this._configuration = workspace.getConfiguration('tsserver') + this._includePackageJsonAutoImports = workspace.getConfiguration('typescript').get<'auto' | 'on' | 'off'>('preferences.includePackageJsonAutoImports') workspace.onDidChangeConfiguration(() => { this._configuration = workspace.getConfiguration('tsserver') + this._includePackageJsonAutoImports = workspace.getConfiguration('typescript').get<'auto' | 'on' | 'off'>('preferences.includePackageJsonAutoImports') }) } + public get enableTsServerTracing(): boolean { + return this._configuration.get('enableTracing', false) + } + + public get includePackageJsonAutoImports(): 'auto' | 'on' | 'off' { + return this._includePackageJsonAutoImports + } + public get locale(): string | null { return this._configuration.get('locale', null) } @@ -97,10 +108,6 @@ export class TypeScriptServiceConfiguration { return this._configuration.get('maxTsServerMemory', 0) } - public get debugPort(): number | null { - return this._configuration.get('debugPort', parseInt(process.env['TSS_DEBUG'], 10)) - } - public get npmLocation(): string | null { let path = this._configuration.get('npm', '') if (path) return workspace.expand(path) diff --git a/src/server/utils/languageDescription.ts b/src/server/utils/languageDescription.ts index 75cd1f9..dfa6c7e 100644 --- a/src/server/utils/languageDescription.ts +++ b/src/server/utils/languageDescription.ts @@ -3,16 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as languageModeIds from './languageModeIds' +import path from 'path' +import { Uri } from 'coc.nvim' export interface LanguageDescription { readonly id: string readonly diagnosticSource: string readonly diagnosticLanguage: DiagnosticLanguage - readonly modeIds: string[] - readonly configFile?: string + readonly languageIds: string[] readonly isExternal?: boolean readonly diagnosticOwner: string readonly configFilePattern?: RegExp + readonly standardFileExtensions: ReadonlyArray, } export const enum DiagnosticLanguage { @@ -25,19 +27,45 @@ export const standardLanguageDescriptions: LanguageDescription[] = [ id: 'typescript', diagnosticSource: 'ts', diagnosticOwner: 'typescript', - modeIds: [languageModeIds.typescript, languageModeIds.typescriptreact, - languageModeIds.typescripttsx, languageModeIds.typescriptjsx], diagnosticLanguage: DiagnosticLanguage.TypeScript, - configFile: 'tsconfig.json', - configFilePattern: /^tsconfig(\..*)?\.json$/gi + languageIds: [languageModeIds.typescript, languageModeIds.typescriptreact, languageModeIds.typescripttsx, languageModeIds.typescriptjsx], + configFilePattern: /^tsconfig(\..*)?\.json$/gi, + standardFileExtensions: [ + 'ts', + 'tsx', + 'cts', + 'mts' + ] }, { id: 'javascript', diagnosticSource: 'ts', diagnosticOwner: 'typescript', - modeIds: [languageModeIds.javascript, languageModeIds.javascriptreact, languageModeIds.javascriptjsx], - diagnosticLanguage: DiagnosticLanguage.JavaScript, - configFile: 'jsconfig.json', - configFilePattern: /^jsconfig(\..*)?\.json$/gi + languageIds: [languageModeIds.javascript, languageModeIds.javascriptreact, languageModeIds.javascriptjsx], diagnosticLanguage: DiagnosticLanguage.JavaScript, + configFilePattern: /^jsconfig(\..*)?\.json$/gi, + standardFileExtensions: [ + 'js', + 'jsx', + 'cjs', + 'mjs', + 'es6', + 'pac', + ] } ] + +export function isTsConfigFileName(fileName: string): boolean { + return /^tsconfig\.(.+\.)?json$/i.test(path.basename(fileName)) +} + +export function isJsConfigOrTsConfigFileName(fileName: string): boolean { + return /^[jt]sconfig\.(.+\.)?json$/i.test(path.basename(fileName)) +} + +export function doesResourceLookLikeATypeScriptFile(resource: Uri): boolean { + return /\.(tsx?|mts|cts)$/i.test(resource.fsPath) +} + +export function doesResourceLookLikeAJavaScriptFile(resource: Uri): boolean { + return /\.(jsx?|mjs|cjs)$/i.test(resource.fsPath) +} diff --git a/src/server/utils/logger.ts b/src/server/utils/logger.ts index 4169cec..0d30ca5 100644 --- a/src/server/utils/logger.ts +++ b/src/server/utils/logger.ts @@ -51,12 +51,24 @@ export default class Logger { this.logLevel('Error', message, data) } + private now(): string { + const now = new Date() + return padLeft(now.getUTCHours() + '', 2, '0') + + ':' + padLeft(now.getMinutes() + '', 2, '0') + + ':' + padLeft(now.getUTCSeconds() + '', 2, '0') + '.' + now.getMilliseconds() + } + public logLevel(level: string, message: string, data?: any): void { this.output.appendLine( - `[${level} - ${new Date().toLocaleTimeString()}] ${message}` + `[${level} - ${this.now()}] ${message}` ) if (data) { this.output.appendLine(this.data2String(data)) } } } + + +function padLeft(s: string, n: number, pad = ' ') { + return pad.repeat(Math.max(0, n - s.length)) + s +} diff --git a/src/server/utils/previewer.ts b/src/server/utils/previewer.ts index ccae4f5..9ea0351 100644 --- a/src/server/utils/previewer.ts +++ b/src/server/utils/previewer.ts @@ -94,20 +94,15 @@ function getTagDocumentation(tag: Proto.JSDocTagInfo): string | undefined { return label + (text.match(/\r\n|\n/g) ? ' \n' + text : ` — ${text}`) } -export function plain(parts: Proto.SymbolDisplayPart[]): string { - if (!parts || !parts.length) return '' - return parts.map(part => part.text).join('') -} - export function tagsMarkdownPreview(tags: Proto.JSDocTagInfo[]): string { return (tags || []).map(getTagDocumentation).join(' \n\n') } export function markdownDocumentation( - documentation: Proto.SymbolDisplayPart[], + documentation: Proto.SymbolDisplayPart[] | string, tags: Proto.JSDocTagInfo[] ): MarkupContent { - let out = plain(documentation) + let out = plainWithLinks(documentation) const tagsPreview = tagsMarkdownPreview(tags) if (tagsPreview) { out = out + ('\n\n' + tagsPreview) @@ -118,6 +113,12 @@ export function markdownDocumentation( } } +export function plainWithLinks( + parts: readonly Proto.SymbolDisplayPart[] | string, +): string { + return processInlineTags(convertLinkTags(parts)) +} + /** * Convert `@link` inline tags to markdown links */ diff --git a/src/server/utils/process.ts b/src/server/utils/process.ts index 3b8c6fd..8580943 100644 --- a/src/server/utils/process.ts +++ b/src/server/utils/process.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import cp from 'child_process' -import net from 'net' import os from 'os' import path from 'path' import fs from 'fs' @@ -24,47 +23,40 @@ export function makeRandomHexString(length: number): string { return result } -export function getTempDirectory(): string { +export function getTempDirectory(): string | undefined { let dir = path.join(os.tmpdir(), `coc.nvim-${process.pid}`) - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir) + try { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir) + } + } catch (e) { + return undefined } return dir } -function generatePipeName(): string { - return getPipeName(makeRandomHexString(40)) -} - -function getPipeName(name: string): string { - const fullName = 'coc-tsc-' + name - if (process.platform === 'win32') { - return '\\\\.\\pipe\\' + fullName + '-sock' - } - const tmpdir = getTempDirectory() - // Mac/Unix: use socket file - return path.join(tmpdir, fullName + '.sock') -} - -export function getTempFile(name: string): string { +export function getTempFile(name: string): string | undefined { const fullName = 'coc-nvim-' + name - return path.join(getTempDirectory(), fullName + '.sock') + let dir = getTempDirectory() + if (!dir) return undefined + return path.join(dir, fullName + '.sock') } -function generatePatchedEnv( - env: any, - stdInPipeName: string, - stdOutPipeName: string, - stdErrPipeName: string -): any { +export function createTempDirectory(name: string) { + let dir = getTempDirectory() + if (!dir) return undefined + let res = path.join(dir, name) + try { + fs.mkdirSync(res) + } catch (e) { + return undefined + } + return res +} + +function generatePatchedEnv(env: any, modulePath: string): any { const newEnv = Object.assign({}, env) - - // Set the two unique pipe names and the electron flag as process env - newEnv['STDIN_PIPE_NAME'] = stdInPipeName // tslint:disable-line - newEnv['STDOUT_PIPE_NAME'] = stdOutPipeName // tslint:disable-line - newEnv['STDERR_PIPE_NAME'] = stdErrPipeName // tslint:disable-line - newEnv['TSS_LOG'] = `-level verbose -file ${path.join(os.tmpdir(), 'coc-nvim-tsc.log')}` // tslint:disable-line - + newEnv['NODE_PATH'] = path.join(modulePath, '..', '..', '..') // Ensure we always have a PATH set newEnv['PATH'] = newEnv['PATH'] || process.env.PATH // tslint:disable-line return newEnv @@ -75,88 +67,14 @@ export function fork( args: string[], options: IForkOptions, logger: Logger, - callback: (error: any, cp: cp.ChildProcess | null) => void -): void { - let callbackCalled = false - const resolve = (result: cp.ChildProcess) => { - if (callbackCalled) { - return - } - callbackCalled = true - callback(null, result) - } - const reject = (err: any) => { - if (callbackCalled) { - return - } - callbackCalled = true - callback(err, null) - } - - // Generate three unique pipe names - const stdInPipeName = generatePipeName() - const stdOutPipeName = generatePipeName() - const stdErrPipeName = generatePipeName() - - const newEnv = generatePatchedEnv( - process.env, - stdInPipeName, - stdOutPipeName, - stdErrPipeName - ) - newEnv['NODE_PATH'] = path.join(modulePath, '..', '..', '..') // tslint:disable-line - - let childProcess: cp.ChildProcess - // Begin listening to stderr pipe - let stdErrServer = net.createServer(stdErrStream => { - // From now on the childProcess.stderr is available for reading - childProcess.stderr = stdErrStream - }) - stdErrServer.listen(stdErrPipeName) - - // Begin listening to stdout pipe - let stdOutServer = net.createServer(stdOutStream => { - // The child process will write exactly one chunk with content `ready` when it has installed a listener to the stdin pipe - - stdOutStream.once('data', (_chunk: Buffer) => { - // The child process is sending me the `ready` chunk, time to connect to the stdin pipe - childProcess.stdin = net.connect(stdInPipeName) as any - - // From now on the childProcess.stdout is available for reading - childProcess.stdout = stdOutStream - - resolve(childProcess) - }) - }) - stdOutServer.listen(stdOutPipeName) - - let serverClosed = false - const closeServer = () => { - if (serverClosed) { - return - } - serverClosed = true - stdOutServer.close() - stdErrServer.close() - } - +): cp.ChildProcess { // Create the process - logger.info('Forking TSServer', `PATH: ${newEnv['PATH']} `) - - const bootstrapperPath = path.resolve(__dirname, '../bin/tsserverForkStart') - childProcess = cp.fork(bootstrapperPath, [modulePath].concat(args), { + logger.info('Forking TSServer', `PATH: ${modulePath} `) + let childProcess = cp.fork(modulePath, args, { silent: true, - env: newEnv, + cwd: undefined, + env: generatePatchedEnv(process.env, modulePath), execArgv: options.execArgv }) - - childProcess.once('error', (err: Error) => { - closeServer() - reject(err) - }) - - childProcess.once('exit', (err: Error) => { - closeServer() - reject(err) - }) + return childProcess } diff --git a/src/server/utils/typeConverters.ts b/src/server/utils/typeConverters.ts index ace1aa1..6a07813 100644 --- a/src/server/utils/typeConverters.ts +++ b/src/server/utils/typeConverters.ts @@ -6,6 +6,7 @@ * Helpers for converting FROM LanguageServer types language-server ts types */ import * as language from 'vscode-languageserver-protocol' +import { TextDocumentEdit } from 'vscode-languageserver-protocol' import Proto from '../protocol' import * as PConst from '../protocol.const' import { ITypeScriptServiceClient } from '../typescriptService' @@ -98,14 +99,20 @@ export namespace WorkspaceEdit { client: ITypeScriptServiceClient, edits: Iterable ): language.WorkspaceEdit { - let changes = {} + let documentChanges: TextDocumentEdit[] = [] for (const edit of edits) { let uri = client.toResource(edit.fileName) - changes[uri] = edit.textChanges.map(change => { - return TextEdit.fromCodeEdit(change) + documentChanges.push({ + textDocument: { + uri, + version: null + }, + edits: edit.textChanges.map(change => { + return TextEdit.fromCodeEdit(change) + }) }) } - return { changes } + return { documentChanges } } } diff --git a/src/server/utils/typingsStatus.ts b/src/server/utils/typingsStatus.ts index 8adf8b9..d1608a5 100644 --- a/src/server/utils/typingsStatus.ts +++ b/src/server/utils/typingsStatus.ts @@ -1,4 +1,4 @@ -import { StatusBarItem, window } from 'coc.nvim' +import { StatusBarItem, workspace, window } from 'coc.nvim' /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. @@ -110,10 +110,19 @@ export class AtaProgressReporter { } } - private onTypesInstallerInitializationFailed() { // tslint:disable-line + private async onTypesInstallerInitializationFailed() { // tslint:disable-line this.statusItem.hide() if (!this._invalid) { - window.showMessage('Could not install typings files for JavaScript language features. Please ensure that NPM is installed', 'error') + const config = workspace.getConfiguration('typescript') + if (config.get('check.npmIsInstalled', true)) { + const dontShowAgain = "Don't Show Again" + const selected = await window.showWarningMessage( + "Could not install typings files for JavaScript language features. Please ensure that NPM is installed or configure 'typescript.npm' in your user settings. visit https://go.microsoft.com/fwlink/?linkid=847635 to learn more.", + dontShowAgain) + if (selected === dontShowAgain) { + config.update('check.npmIsInstalled', false, true) + } + } } this._invalid = true } diff --git a/src/server/utils/versionProvider.ts b/src/server/utils/versionProvider.ts index 02e4a21..b488bef 100644 --- a/src/server/utils/versionProvider.ts +++ b/src/server/utils/versionProvider.ts @@ -106,6 +106,15 @@ export class TypeScriptVersionProvider { return undefined } + public getVersionFromTscPath(tscPath: string): TypeScriptVersion | undefined { + if (!tscPath || !fs.existsSync(tscPath)) return undefined + let libFolder = path.resolve(tscPath, '../../lib') + if (fs.existsSync(libFolder)) { + let version = new TypeScriptVersion(libFolder) + if (version.isValid) return version + } + } + public getLocalVersion(): TypeScriptVersion | undefined { let folders = workspace.workspaceFolders.map(f => Uri.parse(f.uri).fsPath) for (let p of folders) { diff --git a/src/server/utils/wireProtocol.ts b/src/server/utils/wireProtocol.ts index f71b11f..2b24126 100644 --- a/src/server/utils/wireProtocol.ts +++ b/src/server/utils/wireProtocol.ts @@ -2,7 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { disposeAll } from 'coc.nvim' import stream from 'stream' +import { Disposable, Emitter } from 'vscode-languageserver-protocol' const DefaultSize = 8192 const ContentLength = 'Content-Length: ' @@ -99,18 +101,30 @@ export interface ICallback { (data: T): void // tslint:disable-line } -export class Reader { +export class Reader implements Disposable { private readonly buffer: ProtocolBuffer = new ProtocolBuffer() private nextMessageLength = -1 + private disposables: Disposable[] = [] - public constructor( - private readonly readable: stream.Readable, - private readonly callback: ICallback, - private readonly onError: (error: any) => void - ) { - this.readable.on('data', (data: Buffer) => { + private readonly _onError = new Emitter() + public readonly onError = this._onError.event + + private readonly _onData = new Emitter() + public readonly onData = this._onData.event + + public constructor(readable: stream.Readable) { + const onData = (data: Buffer) => { this.onLengthData(data) + } + readable.on('data', onData) + + this.disposables.push({ + dispose: () => { + readable.off('data', onData) + } }) + this.disposables.push(this._onError) + this.disposables.push(this._onData) } private onLengthData(data: Buffer): void { @@ -129,10 +143,14 @@ export class Reader { } this.nextMessageLength = -1 const json = JSON.parse(msg) - this.callback(json) + this._onData.fire(json) } } catch (e) { - this.onError(e) + this._onError.fire(e) } } + + public dispose(): void { + disposeAll(this.disposables) + } } diff --git a/src/utils/fileSchemess.ts b/src/utils/fileSchemes.ts similarity index 100% rename from src/utils/fileSchemess.ts rename to src/utils/fileSchemes.ts diff --git a/tsconfig.json b/tsconfig.json index 6464e65..40d3beb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "allowUnreachableCode": true, "allowUnusedLabels": true, "forceConsistentCasingInFileNames": true, + "noEmit": true, "noImplicitAny": false, "noImplicitReturns": false, "noUnusedLocals": false, diff --git a/yarn.lock b/yarn.lock index a598ef9..75f5677 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,35 +2,153 @@ # yarn lockfile v1 -"@types/node@^10.12.0": - version "10.17.44" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.44.tgz#3945e6b702cb6403f22b779c8ea9e5c3f44ead40" - integrity sha512-vHPAyBX1ffLcy4fQHmDyIUMUb42gHZjPHU66nhvbMzAWJqHnySGZ6STwN3rwrnSd1FHB0DI/RWgGELgKSYRDmw== +"@types/node@^12.12.12": + version "12.20.41" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.41.tgz#81d7734c5257da9f04354bd9084a6ebbdd5198a5" + integrity sha512-f6xOqucbDirG7LOzedpvzjP3UTmHttRou3Mosx3vL9wr9AIQGhcPgVnqa8ihpZYnxyM1rxeNCvTyukPKZtq10Q== -coc.nvim@^0.0.81-next.6: - version "0.0.81-next.6" - resolved "https://registry.yarnpkg.com/coc.nvim/-/coc.nvim-0.0.81-next.6.tgz#c3ee7079a66702ebb3b06d4c2bf333d9306ec561" - integrity sha512-VT+DhygyTIzu9IRrwCUljMzfNfh8TeXqqrvFsBE0E8cUwERgCAIvRbBMEDfqaaI+XFgyuwNRwbX5kEvfjG/u3g== +coc.nvim@^0.0.81-next.25: + version "0.0.81-next.25" + resolved "https://registry.yarnpkg.com/coc.nvim/-/coc.nvim-0.0.81-next.25.tgz#8f84b7c71b742e111d330fb553b0df604d4929ec" + integrity sha512-c0OOZQSjgKLGNhIpKzlxkPiPmMCmYHSVcCDNA26BqFX8X0iWt3xXqwbxKiE54zfIsz0wFqL59iBVGUSBaqHGpA== -esbuild@^0.8.29: - version "0.8.29" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.8.29.tgz#cc20fb752e0905a3546d68ae1be58f9b97044c39" - integrity sha512-UDsEoeXuctVgG2hEts1Hwq2jYDGqV7nksEHEZaiCy2v+lXF5ButX4ErPAJAFi5ZNKKW+6Pom93pArV7hki6HnQ== +esbuild-android-arm64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.11.tgz#b8b34e35a5b43880664ac7a3fbc70243d7ed894f" + integrity sha512-6iHjgvMnC/SzDH8TefL+/3lgCjYWwAd1LixYfmz/TBPbDQlxcuSkX0yiQgcJB9k+ibZ54yjVXziIwGdlc+6WNw== + +esbuild-darwin-64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.11.tgz#ba805de98c0412e50fcd0636451797da157b0625" + integrity sha512-olq84ikh6TiBcrs3FnM4eR5VPPlcJcdW8BnUz/lNoEWYifYQ+Po5DuYV1oz1CTFMw4k6bQIZl8T3yxL+ZT2uvQ== + +esbuild-darwin-arm64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.11.tgz#4d3573e448af76ce33e16231f3d9f878542d6fe8" + integrity sha512-Jj0ieWLREPBYr/TZJrb2GFH8PVzDqiQWavo1pOFFShrcmHWDBDrlDxPzEZ67NF/Un3t6sNNmeI1TUS/fe1xARg== + +esbuild-freebsd-64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.11.tgz#9294e6ab359ec93590ab097b0f2017de7c78ab4d" + integrity sha512-C5sT3/XIztxxz/zwDjPRHyzj/NJFOnakAanXuyfLDwhwupKPd76/PPHHyJx6Po6NI6PomgVp/zi6GRB8PfrOTA== + +esbuild-freebsd-arm64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.11.tgz#ae3e0b09173350b66cf8321583c9a1c1fcb8bb55" + integrity sha512-y3Llu4wbs0bk4cwjsdAtVOesXb6JkdfZDLKMt+v1U3tOEPBdSu6w8796VTksJgPfqvpX22JmPLClls0h5p+L9w== + +esbuild-linux-32@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.11.tgz#ddadbc7038aa5a6b1675bb1503cf79a0cbf1229a" + integrity sha512-Cg3nVsxArjyLke9EuwictFF3Sva+UlDTwHIuIyx8qpxRYAOUTmxr2LzYrhHyTcGOleLGXUXYsnUVwKqnKAgkcg== + +esbuild-linux-64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.11.tgz#d698e3ce3a231ddfeec6b5df8c546ae8883fcd88" + integrity sha512-oeR6dIrrojr8DKVrxtH3xl4eencmjsgI6kPkDCRIIFwv4p+K7ySviM85K66BN01oLjzthpUMvBVfWSJkBLeRbg== + +esbuild-linux-arm64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.11.tgz#85faea9fa99ad355b5e3b283197a4dfd0a110fe7" + integrity sha512-+e6ZCgTFQYZlmg2OqLkg1jHLYtkNDksxWDBWNtI4XG4WxuOCUErLqfEt9qWjvzK3XBcCzHImrajkUjO+rRkbMg== + +esbuild-linux-arm@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.11.tgz#74cbcf0b8a22c8401bcbcd6ebd4cbf2baca8b7b4" + integrity sha512-vcwskfD9g0tojux/ZaTJptJQU3a7YgTYsptK1y6LQ/rJmw7U5QJvboNawqM98Ca3ToYEucfCRGbl66OTNtp6KQ== + +esbuild-linux-mips64le@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.11.tgz#490429211a3233f5cbbd8575b7758b897e42979a" + integrity sha512-Rrs99L+p54vepmXIb87xTG6ukrQv+CzrM8eoeR+r/OFL2Rg8RlyEtCeshXJ2+Q66MXZOgPJaokXJZb9snq28bw== + +esbuild-linux-ppc64le@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.11.tgz#fc79d60710213b5b98345f5b138d48245616827a" + integrity sha512-JyzziGAI0D30Vyzt0HDihp4s1IUtJ3ssV2zx9O/c+U/dhUHVP2TmlYjzCfCr2Q6mwXTeloDcLS4qkyvJtYptdQ== + +esbuild-linux-s390x@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.11.tgz#ca4b93556bbba6cc95b0644f2ee93c982165ba07" + integrity sha512-DoThrkzunZ1nfRGoDN6REwmo8ZZWHd2ztniPVIR5RMw/Il9wiWEYBahb8jnMzQaSOxBsGp0PbyJeVLTUatnlcw== + +esbuild-netbsd-64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.11.tgz#edb340bc6653c88804cac2253e21b74258fce165" + integrity sha512-12luoRQz+6eihKYh1zjrw0CBa2aw3twIiHV/FAfjh2NEBDgJQOY4WCEUEN+Rgon7xmLh4XUxCQjnwrvf8zhACw== + +esbuild-openbsd-64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.11.tgz#caeff5f946f79a60ce7bcf88871ca4c71d3476e8" + integrity sha512-l18TZDjmvwW6cDeR4fmizNoxndyDHamGOOAenwI4SOJbzlJmwfr0jUgjbaXCUuYVOA964siw+Ix+A+bhALWg8Q== + +esbuild-sunos-64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.11.tgz#90ce7e1749c2958a53509b4bae7b8f7d98f276d6" + integrity sha512-bmYzDtwASBB8c+0/HVOAiE9diR7+8zLm/i3kEojUH2z0aIs6x/S4KiTuT5/0VKJ4zk69kXel1cNWlHBMkmavQg== + +esbuild-windows-32@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.11.tgz#d067f4ce15b29efba6336e6a23597120fafe49ec" + integrity sha512-J1Ys5hMid8QgdY00OBvIolXgCQn1ARhYtxPnG6ESWNTty3ashtc4+As5nTrsErnv8ZGUcWZe4WzTP/DmEVX1UQ== + +esbuild-windows-64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.11.tgz#13e86dd37a6cd61a5276fa2d271342d0f74da864" + integrity sha512-h9FmMskMuGeN/9G9+LlHPAoiQk9jlKDUn9yA0MpiGzwLa82E7r1b1u+h2a+InprbSnSLxDq/7p5YGtYVO85Mlg== + +esbuild-windows-arm64@0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.11.tgz#e8edfdf1d712085e6dc3fba18a0c225aaae32b75" + integrity sha512-dZp7Krv13KpwKklt9/1vBFBMqxEQIO6ri7Azf8C+ob4zOegpJmha2XY9VVWP/OyQ0OWk6cEeIzMJwInRZrzBUQ== + +esbuild@^0.14.11: + version "0.14.11" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.11.tgz#ac4acb78907874832afb704c3afe58ad37715c27" + integrity sha512-xZvPtVj6yecnDeFb3KjjCM6i7B5TCAQZT77kkW/CpXTMnd6VLnRPKrUB1XHI1pSq6a4Zcy3BGueQ8VljqjDGCg== + optionalDependencies: + esbuild-android-arm64 "0.14.11" + esbuild-darwin-64 "0.14.11" + esbuild-darwin-arm64 "0.14.11" + esbuild-freebsd-64 "0.14.11" + esbuild-freebsd-arm64 "0.14.11" + esbuild-linux-32 "0.14.11" + esbuild-linux-64 "0.14.11" + esbuild-linux-arm "0.14.11" + esbuild-linux-arm64 "0.14.11" + esbuild-linux-mips64le "0.14.11" + esbuild-linux-ppc64le "0.14.11" + esbuild-linux-s390x "0.14.11" + esbuild-netbsd-64 "0.14.11" + esbuild-openbsd-64 "0.14.11" + esbuild-sunos-64 "0.14.11" + esbuild-windows-32 "0.14.11" + esbuild-windows-64 "0.14.11" + esbuild-windows-arm64 "0.14.11" isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= -semver@^7.3.2: - version "7.3.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" - integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" -typescript@^4.3.5: - version "4.3.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" - integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== +semver@^7.3.5: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + +typescript@^4.7.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.2.tgz#1f9aa2ceb9af87cca227813b4310fff0b51593c4" + integrity sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A== vscode-jsonrpc@6.0.0: version "6.0.0" @@ -56,3 +174,8 @@ which@^2.0.2: integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==