Squashed 'lib/vscode/' content from commit e5a624b788

git-subtree-dir: lib/vscode
git-subtree-split: e5a624b788d92b8d34d1392e4c4d9789406efe8f
This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
commit be3e823608
4649 changed files with 1311795 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
build/**
src/**
test/**
test-workspace/**
out/**
tsconfig.json
extension.webpack.config.js
extension-browser.webpack.config.js
cgmanifest.json
yarn.lock

View File

@@ -0,0 +1,7 @@
# Language Features for TypeScript and JavaScript files
**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled.
## Features
See [TypeScript in Visual Studio Code](https://code.visualstudio.com/docs/languages/typescript) and [JavaScript in Visual Studio Code](https://code.visualstudio.com/docs/languages/javascript) to learn about the features of this extension.

View File

@@ -0,0 +1,138 @@
{
"registrations": [
{
"component": {
"type": "git",
"git": {
"name": "TypeScript-TmLanguage",
"repositoryUrl": "https://github.com/microsoft/TypeScript-TmLanguage",
"commitHash": "3133e3d914db9a2bb8812119f9273727a305f16b"
}
},
"license": "MIT",
"version": "0.1.8"
},
{
"component": {
"type": "git",
"git": {
"name": "definitelytyped",
"repositoryUrl": "https://github.com/DefinitelyTyped/DefinitelyTyped",
"commitHash": "69e3ac6bec3008271f76bbfa7cf69aa9198c4ff0"
}
},
"license": "MIT"
},
{
"component": {
"type": "other",
"other": {
"name": "Unicode",
"downloadUrl": "https://home.unicode.org/",
"version": "12.0.0"
}
},
"licenseDetail": [
"Unicode Data Files include all data files under the directories",
"http://www.unicode.org/Public/, http://www.unicode.org/reports/,",
"http://www.unicode.org/cldr/data/, http://source.icu-project.org/repos/icu/, and",
"http://www.unicode.org/utility/trac/browser/.",
"",
"Unicode Data Files do not include PDF online code charts under the",
"directory http://www.unicode.org/Public/.",
"",
"Software includes any source code published in the Unicode Standard",
"or under the directories",
"http://www.unicode.org/Public/, http://www.unicode.org/reports/,",
"http://www.unicode.org/cldr/data/, http://source.icu-project.org/repos/icu/, and",
"http://www.unicode.org/utility/trac/browser/.",
"",
"NOTICE TO USER: Carefully read the following legal agreement.",
"BY DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S",
"DATA FILES (\"DATA FILES\"), AND/OR SOFTWARE (\"SOFTWARE\"),",
"YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE",
"TERMS AND CONDITIONS OF THIS AGREEMENT.",
"IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE",
"THE DATA FILES OR SOFTWARE.",
"",
"COPYRIGHT AND PERMISSION NOTICE",
"",
"Copyright (c) 1991-2017 Unicode, Inc. All rights reserved.",
"Distributed under the Terms of Use in http://www.unicode.org/copyright.html.",
"",
"Permission is hereby granted, free of charge, to any person obtaining",
"a copy of the Unicode data files and any associated documentation",
"(the \"Data Files\") or Unicode software and any associated documentation",
"(the \"Software\") to deal in the Data Files or Software",
"without restriction, including without limitation the rights to use,",
"copy, modify, merge, publish, distribute, and/or sell copies of",
"the Data Files or Software, and to permit persons to whom the Data Files",
"or Software are furnished to do so, provided that either",
"(a) this copyright and permission notice appear with all copies",
"of the Data Files or Software, or",
"(b) this copyright and permission notice appear in associated",
"Documentation.",
"",
"THE DATA FILES AND SOFTWARE ARE PROVIDED \"AS IS\", WITHOUT WARRANTY OF",
"ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE",
"WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND",
"NONINFRINGEMENT OF THIRD PARTY RIGHTS.",
"IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS",
"NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL",
"DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,",
"DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER",
"TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR",
"PERFORMANCE OF THE DATA FILES OR SOFTWARE.",
"",
"Except as contained in this notice, the name of a copyright holder",
"shall not be used in advertising or otherwise to promote the sale,",
"use or other dealings in these Data Files or Software without prior",
"written authorization of the copyright holder."
],
"version": "12.0.0",
"license": "UNICODE, INC. LICENSE AGREEMENT - DATA FILES AND SOFTWARE"
},
{
"component": {
"type": "other",
"other": {
"name": "Document Object Model",
"downloadUrl": "https://www.w3.org/DOM/",
"version": "4.0.0"
}
},
"licenseDetail": [
"W3C License",
"This work is being provided by the copyright holders under the following license.",
"By obtaining and/or copying this work, you (the licensee) agree that you have read, understood, and will comply with the following terms and conditions.",
"Permission to copy, modify, and distribute this work, with or without modification, for any purpose and without fee or royalty is hereby granted, provided that you include the following ",
"on ALL copies of the work or portions thereof, including modifications:",
"* The full text of this NOTICE in a location viewable to users of the redistributed or derivative work.",
"* Any pre-existing intellectual property disclaimers, notices, or terms and conditions. If none exist, the W3C Software and Document Short Notice should be included.",
"* Notice of any changes or modifications, through a copyright statement on the new code or document such as \"This software or document includes material copied from or derived ",
"from Document Object Model. Copyright © 2015 W3C® (MIT, ERCIM, Keio, Beihang).\" ",
"Disclaimers",
"THIS WORK IS PROVIDED \"AS IS",
" AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR ",
"FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.",
"COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT.",
"The name and trademarks of copyright holders may NOT be used in advertising or publicity pertaining to the work without specific, written prior permission. ",
"Title to copyright in this work will at all times remain with copyright holders."
],
"license": "W3C License",
"version": "4.0.0"
},
{
"component": {
"type": "git",
"git": {
"name": "Web Background Synchronization",
"repositoryUrl": "https://github.com/WICG/BackgroundSync",
"commitHash": "10778afe95b5d46c99f7a77565328b7108091510"
}
},
"license": "Apache2"
}
],
"version": 1
}

View File

@@ -0,0 +1,48 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const CopyPlugin = require('copy-webpack-plugin');
const { lchmod } = require('graceful-fs');
const Terser = require('terser');
const withBrowserDefaults = require('../shared.webpack.config').browser;
module.exports = withBrowserDefaults({
context: __dirname,
entry: {
extension: './src/extension.browser.ts',
},
plugins: [
// @ts-ignore
new CopyPlugin({
patterns: [
{
from: 'node_modules/typescript-web-server/*.d.ts',
to: 'typescript-web/',
flatten: true
},
],
}),
// @ts-ignore
new CopyPlugin({
patterns: [
{
from: 'node_modules/typescript-web-server/tsserver.js',
to: 'typescript-web/tsserver.web.js',
transform: (content) => {
return Terser.minify(content.toString()).code;
},
transformPath: (targetPath) => {
return targetPath.replace('tsserver.js', 'tsserver.web.js');
}
}
],
}),
],
});

View File

@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const withDefaults = require('../shared.webpack.config');
module.exports = withDefaults({
context: __dirname,
resolve: {
mainFields: ['module', 'main']
},
externals: {
'typescript-vscode-sh-plugin': 'commonjs vscode' // used by build/lib/extensions to know what node_modules to bundle
},
entry: {
extension: './src/extension.ts',
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

View File

@@ -0,0 +1,34 @@
{
"comments": {
"lineComment": "//",
"blockComment": [ "/*", "*/" ]
},
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "'", "close": "'", "notIn": ["string", "comment"] },
{ "open": "\"", "close": "\"", "notIn": ["string"] },
{ "open": "`", "close": "`", "notIn": ["string", "comment"] },
{ "open": "/**", "close": " */", "notIn": ["string"] }
],
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["'", "'"],
["\"", "\""],
["`", "`"]
],
"folding": {
"markers": {
"start": "^\\s*//\\s*#?region\\b",
"end": "^\\s*//\\s*#?endregion\\b"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,133 @@
{
"displayName": "TypeScript and JavaScript Language Features",
"description": "Provides rich language support for JavaScript and TypeScript.",
"reloadProjects.title": "Reload Project",
"configuration.typescript": "TypeScript",
"configuration.suggest.completeFunctionCalls": "Complete functions with their parameter signature.",
"configuration.suggest.includeAutomaticOptionalChainCompletions": "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.",
"typescript.tsdk.desc": "Specifies the folder path to the tsserver and lib*.d.ts files under a TypeScript install to use for IntelliSense, for example: `./node_modules/typescript/lib`.\n\n- When specified as a user setting, the TypeScript version from `typescript.tsdk` automatically replaces the built-in TypeScript version.\n- When specified as a workspace setting, `typescript.tsdk` allows you to switch to use that workspace version of TypeScript for IntelliSense with the `TypeScript: Select TypeScript version` command.\n\nSee the [TypeScript documentation](https://code.visualstudio.com/docs/typescript/typescript-compiling#_using-newer-typescript-versions) for more detail about managing TypeScript versions.",
"typescript.disableAutomaticTypeAcquisition": "Disables automatic type acquisition. Automatic type acquisition fetches `@types` packages from npm to improve IntelliSense for external libraries.",
"typescript.enablePromptUseWorkspaceTsdk": "Enables prompting of users to use the TypeScript version configured in the workspace for Intellisense.",
"typescript.tsserver.log": "Enables logging of the TS server to a file. This log can be used to diagnose TS Server issues. The log may contain file paths, source code, and other potentially sensitive information from your project.",
"typescript.tsserver.pluginPaths": "Additional paths to discover TypeScript Language Service plugins.",
"typescript.tsserver.pluginPaths.item": "Either an absolute or relative path. Relative path will be resolved against workspace folder(s).",
"typescript.tsserver.trace": "Enables tracing of messages sent to the TS server. This trace can be used to diagnose TS Server issues. The trace may contain file paths, source code, and other potentially sensitive information from your project.",
"typescript.validate.enable": "Enable/disable TypeScript validation.",
"typescript.format.enable": "Enable/disable default TypeScript formatter.",
"javascript.format.enable": "Enable/disable default JavaScript formatter.",
"format.insertSpaceAfterCommaDelimiter": "Defines space handling after a comma delimiter.",
"format.insertSpaceAfterConstructor": "Defines space handling after the constructor keyword.",
"format.insertSpaceAfterSemicolonInForStatements": "Defines space handling after a semicolon in a for statement.",
"format.insertSpaceBeforeAndAfterBinaryOperators": "Defines space handling after a binary operator.",
"format.insertSpaceAfterKeywordsInControlFlowStatements": "Defines space handling after keywords in a control flow statement.",
"format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": "Defines space handling after function keyword for anonymous functions.",
"format.insertSpaceBeforeFunctionParenthesis": "Defines space handling before function argument parentheses.",
"format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": "Defines space handling after opening and before closing non-empty parenthesis.",
"format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": "Defines space handling after opening and before closing non-empty brackets.",
"format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": "Defines space handling after opening and before closing non-empty braces.",
"format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": "Defines space handling after opening and before closing empty braces.",
"format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": "Defines space handling after opening and before closing template string braces.",
"format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": "Defines space handling after opening and before closing JSX expression braces.",
"format.insertSpaceAfterTypeAssertion": "Defines space handling after type assertions in TypeScript.",
"format.placeOpenBraceOnNewLineForFunctions": "Defines whether an open brace is put onto a new line for functions or not.",
"format.placeOpenBraceOnNewLineForControlBlocks": "Defines whether an open brace is put onto a new line for control blocks or not.",
"format.semicolons": "Defines handling of optional semicolons. Requires using TypeScript 3.7 or newer in the workspace.",
"format.semicolons.ignore": "Don't insert or remove any semicolons.",
"format.semicolons.insert": "Insert semicolons at statement ends.",
"format.semicolons.remove": "Remove unnecessary semicolons.",
"javascript.validate.enable": "Enable/disable JavaScript validation.",
"goToProjectConfig.title": "Go to Project Configuration",
"javascript.referencesCodeLens.enabled": "Enable/disable references CodeLens in JavaScript files.",
"javascript.referencesCodeLens.showOnAllFunctions": "Enable/disable references CodeLens on all functions in JavaScript files.",
"typescript.referencesCodeLens.enabled": "Enable/disable references CodeLens in TypeScript files.",
"typescript.referencesCodeLens.showOnAllFunctions": "Enable/disable references CodeLens on all functions in TypeScript files.",
"typescript.implementationsCodeLens.enabled": "Enable/disable implementations CodeLens. This CodeLens shows the implementers of an interface.",
"typescript.openTsServerLog.title": "Open TS Server log",
"typescript.restartTsServer": "Restart TS server",
"typescript.selectTypeScriptVersion.title": "Select TypeScript Version...",
"typescript.reportStyleChecksAsWarnings": "Report style checks as warnings.",
"javascript.implicitProjectConfig.checkJs": "Enable/disable semantic checking of JavaScript files. Existing jsconfig.json or tsconfig.json files override this setting.",
"typescript.npm": "Specifies the path to the npm executable used for Automatic Type Acquisition.",
"typescript.check.npmIsInstalled": "Check if npm is installed for Automatic Type Acquisition.",
"configuration.suggest.names": "Enable/disable including unique names from the file in JavaScript suggestions. Note that name suggestions are always disabled in JavaScript code that is semantically checked using `@ts-check` or `checkJs`.",
"typescript.tsc.autoDetect": "Controls auto detection of tsc tasks.",
"typescript.tsc.autoDetect.off": "Disable this feature.",
"typescript.tsc.autoDetect.on": "Create both build and watch tasks.",
"typescript.tsc.autoDetect.build": "Only create single run compile tasks.",
"typescript.tsc.autoDetect.watch": "Only create compile and watch tasks.",
"typescript.problemMatchers.tsc.label": "TypeScript problems",
"typescript.problemMatchers.tscWatch.label": "TypeScript problems (watch mode)",
"configuration.suggest.paths": "Enable/disable suggestions for paths in import statements and require calls.",
"configuration.tsserver.useSeparateSyntaxServer": "Enable/disable spawning a separate TypeScript server that can more quickly respond to syntax related operations, such as calculating folding or computing document symbols. Requires using TypeScript 3.4.0 or newer in the workspace.",
"configuration.tsserver.maxTsServerMemory": "Set the maximum amount of memory (in MB) to allocate to the TypeScript server process",
"configuration.tsserver.experimental.enableProjectDiagnostics": "(Experimental) Enables project wide error reporting.",
"typescript.locale": "Sets the locale used to report JavaScript and TypeScript errors. Default of `null` uses VS Code's locale.",
"javascript.implicitProjectConfig.experimentalDecorators": "Enable/disable `experimentalDecorators` for JavaScript files that are not part of a project. Existing jsconfig.json or tsconfig.json files override this setting.",
"configuration.suggest.autoImports": "Enable/disable auto import suggestions.",
"taskDefinition.tsconfig.description": "The tsconfig file that defines the TS build.",
"javascript.suggestionActions.enabled": "Enable/disable suggestion diagnostics for JavaScript files in the editor.",
"typescript.suggestionActions.enabled": "Enable/disable suggestion diagnostics for TypeScript files in the editor.",
"typescript.preferences.quoteStyle": "Preferred quote style to use for quick fixes: `single` quotes, `double` quotes, or `auto` infer quote type from existing imports.",
"typescript.preferences.importModuleSpecifier": "Preferred path style for auto imports.",
"typescript.preferences.importModuleSpecifier.auto": "Automatically select import path style. Prefers using a relative import if `baseUrl` is configured and the relative path has fewer segments than the non-relative import.",
"typescript.preferences.importModuleSpecifier.relative": "Relative to the file location.",
"typescript.preferences.importModuleSpecifier.nonRelative": "Based on the `baseUrl` configured in your `jsconfig.json` / `tsconfig.json`.",
"typescript.preferences.importModuleSpecifierEnding": "Preferred path ending for auto imports.",
"typescript.preferences.importModuleSpecifierEnding.auto": "Use project settings to select a default.",
"typescript.preferences.importModuleSpecifierEnding.minimal": "Shorten `./component/index.js` to `./component`.",
"typescript.preferences.importModuleSpecifierEnding.index": "Shorten `./component/index.js` to `./component/index`.",
"typescript.preferences.importModuleSpecifierEnding.js": "Do not shorten path endings; include the `.js` extension.",
"typescript.preferences.includePackageJsonAutoImports": "Enable/disable searching `package.json` dependencies for available auto imports.",
"typescript.preferences.includePackageJsonAutoImports.auto": "Search dependencies based on estimated performance impact.",
"typescript.preferences.includePackageJsonAutoImports.on": "Always search dependencies.",
"typescript.preferences.includePackageJsonAutoImports.off": "Never search dependencies.",
"typescript.updateImportsOnFileMove.enabled": "Enable/disable automatic updating of import paths when you rename or move a file in VS Code.",
"typescript.updateImportsOnFileMove.enabled.prompt": "Prompt on each rename.",
"typescript.updateImportsOnFileMove.enabled.always": "Always update paths automatically.",
"typescript.updateImportsOnFileMove.enabled.never": "Never rename paths and don't prompt.",
"typescript.autoClosingTags": "Enable/disable automatic closing of JSX tags.",
"typescript.suggest.enabled": "Enabled/disable autocomplete suggestions.",
"configuration.surveys.enabled": "Enabled/disable occasional surveys that help us improve VS Code's JavaScript and TypeScript support.",
"configuration.suggest.completeJSDocs": "Enable/disable suggestion to complete JSDoc comments.",
"configuration.tsserver.watchOptions": "Configure which watching strategies should be used to keep track of files and directories. Requires using TypeScript 3.8+ in the workspace.",
"configuration.tsserver.watchOptions.watchFile": "Strategy for how individual files are watched.",
"configuration.tsserver.watchOptions.watchFile.fixedPollingInterval": "Check every file for changes several times a second at a fixed interval.",
"configuration.tsserver.watchOptions.watchFile.priorityPollingInterval": "Check every file for changes several times a second, but use heuristics to check certain types of files less frequently than others.",
"configuration.tsserver.watchOptions.watchFile.dynamicPriorityPolling": "Use a dynamic queue where less-frequently modified files will be checked less often.",
"configuration.tsserver.watchOptions.watchFile.useFsEvents": "Attempt to use the operating system/file systems native events for file changes.",
"configuration.tsserver.watchOptions.watchFile.useFsEventsOnParentDirectory": "Attempt to use the operating system/file systems native events to listen for changes on a files containing directories. This can use fewer file watchers, but might be less accurate.",
"configuration.tsserver.watchOptions.watchDirectory": "Strategy for how entire directory trees are watched under systems that lack recursive file-watching functionality.",
"configuration.tsserver.watchOptions.watchDirectory.fixedPollingInterval": "Check every directory for changes several times a second at a fixed interval.",
"configuration.tsserver.watchOptions.watchDirectory.dynamicPriorityPolling": "Use a dynamic queue where less-frequently modified directories will be checked less often.",
"configuration.tsserver.watchOptions.watchDirectory.useFsEvents": "Attempt to use the operating system/file systems native events for directory changes.",
"configuration.tsserver.watchOptions.fallbackPolling": "When using file system events, this option specifies the polling strategy that gets used when the system runs out of native file watchers and/or doesnt support native file watchers.",
"configuration.tsserver.watchOptions.fallbackPolling.fixedPollingInterval": "Check every file for changes several times a second at a fixed interval.",
"configuration.tsserver.watchOptions.fallbackPolling.priorityPollingInterval": "Check every file for changes several times a second, but use heuristics to check certain types of files less frequently than others.",
"configuration.tsserver.watchOptions.fallbackPolling.dynamicPriorityPolling ": "Use a dynamic queue where less-frequently modified files will be checked less often.",
"configuration.tsserver.watchOptions.synchronousWatchDirectory": "Disable deferred watching on directories. Deferred watching is useful when lots of file changes might occur at once (e.g. a change in node_modules from running npm install), but you might want to disable it with this flag for some less-common setups.",
"typescript.preferences.renameShorthandProperties.deprecationMessage": "The setting 'typescript.preferences.renameShorthandProperties' has been deprecated in favor of 'typescript.preferences.useAliasesForRenames'",
"typescript.preferences.useAliasesForRenames": "Enable/disable introducing aliases for object shorthand properties during renames. Requires using TypeScript 3.4 or newer in the workspace.",
"typescript.workspaceSymbols.scope": "Controls which files are searched by [go to symbol in workspace](https://code.visualstudio.com/docs/editor/editingevolved#_open-symbol-by-name).",
"typescript.workspaceSymbols.scope.allOpenProjects": "Search all open JavaScript or TypeScript projects for symbols. Requires using TypeScript 3.9 or newer in the workspace.",
"typescript.workspaceSymbols.scope.currentProject": "Only search for symbols in the current JavaScript or TypeScript project.",
"codeActions.refactor.extract.constant.title": "Extract constant",
"codeActions.refactor.extract.constant.description": "Extract expression to constant.",
"codeActions.refactor.extract.function.title": "Extract function",
"codeActions.refactor.extract.function.description": "Extract expression to method or function.",
"codeActions.refactor.extract.type.title": "Extract type",
"codeActions.refactor.extract.type.description": "Extract type to a type alias.",
"codeActions.refactor.extract.interface.title": "Extract interface",
"codeActions.refactor.extract.interface.description": "Extract type to an interface.",
"codeActions.refactor.rewrite.import.title": "Convert import",
"codeActions.refactor.rewrite.import.description": "Convert between named imports and namespace imports.",
"codeActions.refactor.rewrite.export.title": "Convert export",
"codeActions.refactor.rewrite.export.description": "Convert between default export and named export.",
"codeActions.refactor.move.newFile.title": "Move to a new file",
"codeActions.refactor.move.newFile.description": "Move the expression to a new file.",
"codeActions.refactor.rewrite.arrow.braces.title": "Rewrite arrow braces",
"codeActions.refactor.rewrite.arrow.braces.description": "Add or remove braces in an arrow function.",
"codeActions.refactor.rewrite.parameters.toDestructured.title": "Convert parameters to destructured object",
"codeActions.refactor.rewrite.property.generateAccessors.title": "Generate accessors",
"codeActions.refactor.rewrite.property.generateAccessors.description": "Generate 'get' and 'set' accessors",
"codeActions.source.organizeImports.title": "Organize imports"
}

View File

@@ -0,0 +1,10 @@
{
"allowTrailingCommas": true,
"title": "JSON schema for the JavaScript configuration file",
"type": "object",
"default": {
"compilerOptions": {
"target": "es6"
}
}
}

View File

@@ -0,0 +1,31 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "TypeScript contributions to package.json",
"type": "object",
"properties": {
"contributes": {
"type": "object",
"properties": {
"typescriptServerPlugins": {
"type": "array",
"description": "Contributed TypeScript server plugins.",
"items": {
"type": "object",
"description": "TypeScript server plugin.",
"properties": {
"name": {
"type": "string",
"description": "Name of the plugin as listed in the package.json."
},
"enableForWorkspaceTypeScriptVersions": {
"type": "boolean",
"default": false,
"description": "Should the plugin be loaded when using workspace versions of TypeScript?"
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,13 @@
{
"allowTrailingCommas": true,
"title": "JSON schema for the TypeScript compiler's configuration file",
"type": "object",
"default": {
"compilerOptions": {
"module": "commonjs"
},
"exclude": [
"node_modules"
]
}
}

View File

@@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { PluginManager } from './utils/plugins';
class ApiV0 {
public constructor(
public readonly onCompletionAccepted: vscode.Event<vscode.CompletionItem & { metadata?: any }>,
private readonly _pluginManager: PluginManager,
) { }
configurePlugin(pluginId: string, configuration: {}): void {
this._pluginManager.setConfiguration(pluginId, configuration);
}
}
export interface Api {
getAPI(version: 0): ApiV0 | undefined;
}
export function getExtensionApi(
onCompletionAccepted: vscode.Event<vscode.CompletionItem>,
pluginManager: PluginManager,
): Api {
return {
getAPI(version) {
if (version === 0) {
return new ApiV0(onCompletionAccepted, pluginManager);
}
return undefined;
}
};
}

View File

@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
export interface Command {
readonly id: string | string[];
execute(...args: any[]): void;
}
export class CommandManager {
private readonly commands = new Map<string, vscode.Disposable>();
public dispose() {
for (const registration of this.commands.values()) {
registration.dispose();
}
this.commands.clear();
}
public register<T extends Command>(command: T): T {
for (const id of Array.isArray(command.id) ? command.id : [command.id]) {
this.registerCommand(id, command.execute, command);
}
return command;
}
private registerCommand(id: string, impl: (...args: any[]) => void, thisArg?: any) {
if (this.commands.has(id)) {
return;
}
this.commands.set(id, vscode.commands.registerCommand(id, impl, thisArg));
}
}

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { PluginManager } from '../utils/plugins';
import { Command } from './commandManager';
export class ConfigurePluginCommand implements Command {
public readonly id = '_typescript.configurePlugin';
public constructor(
private readonly pluginManager: PluginManager,
) { }
public execute(pluginId: string, configuration: any) {
this.pluginManager.setConfiguration(pluginId, configuration);
}
}

View File

@@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import TypeScriptServiceClientHost from '../typeScriptServiceClientHost';
import { Lazy } from '../utils/lazy';
import { openProjectConfigForFile, ProjectType } from '../utils/tsconfig';
import { Command } from './commandManager';
export class TypeScriptGoToProjectConfigCommand implements Command {
public readonly id = 'typescript.goToProjectConfig';
public constructor(
private readonly lazyClientHost: Lazy<TypeScriptServiceClientHost>,
) { }
public execute() {
const editor = vscode.window.activeTextEditor;
if (editor) {
openProjectConfigForFile(ProjectType.TypeScript, this.lazyClientHost.value.serviceClient, editor.document.uri);
}
}
}
export class JavaScriptGoToProjectConfigCommand implements Command {
public readonly id = 'javascript.goToProjectConfig';
public constructor(
private readonly lazyClientHost: Lazy<TypeScriptServiceClientHost>,
) { }
public execute() {
const editor = vscode.window.activeTextEditor;
if (editor) {
openProjectConfigForFile(ProjectType.JavaScript, this.lazyClientHost.value.serviceClient, editor.document.uri);
}
}
}

View File

@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import TypeScriptServiceClientHost from '../typeScriptServiceClientHost';
import { Lazy } from '../utils/lazy';
import { PluginManager } from '../utils/plugins';
import { CommandManager } from './commandManager';
import { ConfigurePluginCommand } from './configurePlugin';
import { JavaScriptGoToProjectConfigCommand, TypeScriptGoToProjectConfigCommand } from './goToProjectConfiguration';
import { LearnMoreAboutRefactoringsCommand } from './learnMoreAboutRefactorings';
import { OpenTsServerLogCommand } from './openTsServerLog';
import { ReloadJavaScriptProjectsCommand, ReloadTypeScriptProjectsCommand } from './reloadProject';
import { RestartTsServerCommand } from './restartTsServer';
import { SelectTypeScriptVersionCommand } from './selectTypeScriptVersion';
export function registerBaseCommands(
commandManager: CommandManager,
lazyClientHost: Lazy<TypeScriptServiceClientHost>,
pluginManager: PluginManager
): void {
commandManager.register(new ReloadTypeScriptProjectsCommand(lazyClientHost));
commandManager.register(new ReloadJavaScriptProjectsCommand(lazyClientHost));
commandManager.register(new SelectTypeScriptVersionCommand(lazyClientHost));
commandManager.register(new OpenTsServerLogCommand(lazyClientHost));
commandManager.register(new RestartTsServerCommand(lazyClientHost));
commandManager.register(new TypeScriptGoToProjectConfigCommand(lazyClientHost));
commandManager.register(new JavaScriptGoToProjectConfigCommand(lazyClientHost));
commandManager.register(new ConfigurePluginCommand(pluginManager));
commandManager.register(new LearnMoreAboutRefactoringsCommand());
}

View File

@@ -0,0 +1,21 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { isTypeScriptDocument } from '../utils/languageModeIds';
import { Command } from './commandManager';
export class LearnMoreAboutRefactoringsCommand implements Command {
public static readonly id = '_typescript.learnMoreAboutRefactorings';
public readonly id = LearnMoreAboutRefactoringsCommand.id;
public execute() {
const docUrl = vscode.window.activeTextEditor && isTypeScriptDocument(vscode.window.activeTextEditor.document)
? 'https://go.microsoft.com/fwlink/?linkid=2114477'
: 'https://go.microsoft.com/fwlink/?linkid=2116761';
vscode.env.openExternal(vscode.Uri.parse(docUrl));
}
}

View File

@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import TypeScriptServiceClientHost from '../typeScriptServiceClientHost';
import { Lazy } from '../utils/lazy';
import { Command } from './commandManager';
export class OpenTsServerLogCommand implements Command {
public readonly id = 'typescript.openTsServerLog';
public constructor(
private readonly lazyClientHost: Lazy<TypeScriptServiceClientHost>
) { }
public execute() {
this.lazyClientHost.value.serviceClient.openTsServerLogFile();
}
}

View File

@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import TypeScriptServiceClientHost from '../typeScriptServiceClientHost';
import { Lazy } from '../utils/lazy';
import { Command } from './commandManager';
export class ReloadTypeScriptProjectsCommand implements Command {
public readonly id = 'typescript.reloadProjects';
public constructor(
private readonly lazyClientHost: Lazy<TypeScriptServiceClientHost>
) { }
public execute() {
this.lazyClientHost.value.reloadProjects();
}
}
export class ReloadJavaScriptProjectsCommand implements Command {
public readonly id = 'javascript.reloadProjects';
public constructor(
private readonly lazyClientHost: Lazy<TypeScriptServiceClientHost>
) { }
public execute() {
this.lazyClientHost.value.reloadProjects();
}
}

View File

@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import TypeScriptServiceClientHost from '../typeScriptServiceClientHost';
import { Lazy } from '../utils/lazy';
import { Command } from './commandManager';
export class RestartTsServerCommand implements Command {
public readonly id = 'typescript.restartTsServer';
public constructor(
private readonly lazyClientHost: Lazy<TypeScriptServiceClientHost>
) { }
public execute() {
this.lazyClientHost.value.serviceClient.restartTsServer();
}
}

View File

@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import TypeScriptServiceClientHost from '../typeScriptServiceClientHost';
import { Lazy } from '../utils/lazy';
import { Command } from './commandManager';
export class SelectTypeScriptVersionCommand implements Command {
public readonly id = 'typescript.selectTypeScriptVersion';
public constructor(
private readonly lazyClientHost: Lazy<TypeScriptServiceClientHost>
) { }
public execute() {
this.lazyClientHost.value.serviceClient.showVersionPicker();
}
}

View File

@@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { Api, getExtensionApi } from './api';
import { registerBaseCommands } from './commands/index';
import { LanguageConfigurationManager } from './languageFeatures/languageConfiguration';
import { createLazyClientHost, lazilyActivateClient } from './lazyClientHost';
import { noopRequestCancellerFactory } from './tsServer/cancellation';
import { noopLogDirectoryProvider } from './tsServer/logDirectoryProvider';
import { ITypeScriptVersionProvider, TypeScriptVersion, TypeScriptVersionSource } from './tsServer/versionProvider';
import { WorkerServerProcess } from './tsServer/serverProcess.browser';
import API from './utils/api';
import { CommandManager } from './commands/commandManager';
import { TypeScriptServiceConfiguration } from './utils/configuration';
import { PluginManager } from './utils/plugins';
class StaticVersionProvider implements ITypeScriptVersionProvider {
constructor(
private readonly _version: TypeScriptVersion
) { }
updateConfiguration(_configuration: TypeScriptServiceConfiguration): void {
// noop
}
get defaultVersion() { return this._version; }
get bundledVersion() { return this._version; }
readonly globalVersion = undefined;
readonly localVersion = undefined;
readonly localVersions = [];
}
export function activate(
context: vscode.ExtensionContext
): Api {
const pluginManager = new PluginManager();
context.subscriptions.push(pluginManager);
const commandManager = new CommandManager();
context.subscriptions.push(commandManager);
context.subscriptions.push(new LanguageConfigurationManager());
const onCompletionAccepted = new vscode.EventEmitter<vscode.CompletionItem>();
context.subscriptions.push(onCompletionAccepted);
const versionProvider = new StaticVersionProvider(
new TypeScriptVersion(
TypeScriptVersionSource.Bundled,
vscode.Uri.joinPath(context.extensionUri, 'dist/browser/typescript-web/tsserver.web.js').toString(),
API.fromSimpleString('4.0.3')));
const lazyClientHost = createLazyClientHost(context, false, {
pluginManager,
commandManager,
logDirectoryProvider: noopLogDirectoryProvider,
cancellerFactory: noopRequestCancellerFactory,
versionProvider,
processFactory: WorkerServerProcess
}, item => {
onCompletionAccepted.fire(item);
});
registerBaseCommands(commandManager, lazyClientHost, pluginManager);
// context.subscriptions.push(task.register(lazyClientHost.map(x => x.serviceClient)));
import('./languageFeatures/tsconfig').then(module => {
context.subscriptions.push(module.register());
});
context.subscriptions.push(lazilyActivateClient(lazyClientHost, pluginManager));
return getExtensionApi(onCompletionAccepted.event, pluginManager);
}

View File

@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as rimraf from 'rimraf';
import * as vscode from 'vscode';
import { Api, getExtensionApi } from './api';
import { registerBaseCommands } from './commands/index';
import { LanguageConfigurationManager } from './languageFeatures/languageConfiguration';
import { createLazyClientHost, lazilyActivateClient } from './lazyClientHost';
import { nodeRequestCancellerFactory } from './tsServer/cancellation.electron';
import { NodeLogDirectoryProvider } from './tsServer/logDirectoryProvider.electron';
import { ChildServerProcess } from './tsServer/serverProcess.electron';
import { DiskTypeScriptVersionProvider } from './tsServer/versionProvider.electron';
import { CommandManager } from './commands/commandManager';
import { onCaseInsenitiveFileSystem } from './utils/fileSystem.electron';
import { PluginManager } from './utils/plugins';
import * as temp from './utils/temp.electron';
export function activate(
context: vscode.ExtensionContext
): Api {
const pluginManager = new PluginManager();
context.subscriptions.push(pluginManager);
const commandManager = new CommandManager();
context.subscriptions.push(commandManager);
const onCompletionAccepted = new vscode.EventEmitter<vscode.CompletionItem>();
context.subscriptions.push(onCompletionAccepted);
const logDirectoryProvider = new NodeLogDirectoryProvider(context);
const versionProvider = new DiskTypeScriptVersionProvider();
context.subscriptions.push(new LanguageConfigurationManager());
const lazyClientHost = createLazyClientHost(context, onCaseInsenitiveFileSystem(), {
pluginManager,
commandManager,
logDirectoryProvider,
cancellerFactory: nodeRequestCancellerFactory,
versionProvider,
processFactory: ChildServerProcess,
}, item => {
onCompletionAccepted.fire(item);
});
registerBaseCommands(commandManager, lazyClientHost, pluginManager);
import('./task/taskProvider').then(module => {
context.subscriptions.push(module.register(lazyClientHost.map(x => x.serviceClient)));
});
import('./languageFeatures/tsconfig').then(module => {
context.subscriptions.push(module.register());
});
context.subscriptions.push(lazilyActivateClient(lazyClientHost, pluginManager));
return getExtensionApi(onCompletionAccepted.event, pluginManager);
}
export function deactivate() {
rimraf.sync(temp.getInstanceTempDir());
}

View File

@@ -0,0 +1,125 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import * as PConst from '../protocol.const';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { conditionalRegistration, requireSomeCapability, requireMinVersion } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import { parseKindModifier } from '../utils/modifiers';
import * as typeConverters from '../utils/typeConverters';
class TypeScriptCallHierarchySupport implements vscode.CallHierarchyProvider {
public static readonly minVersion = API.v380;
public constructor(
private readonly client: ITypeScriptServiceClient
) { }
public async prepareCallHierarchy(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.CallHierarchyItem | vscode.CallHierarchyItem[] | undefined> {
const filepath = this.client.toOpenedFilePath(document);
if (!filepath) {
return undefined;
}
const args = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
const response = await this.client.execute('prepareCallHierarchy', args, token);
if (response.type !== 'response' || !response.body) {
return undefined;
}
return Array.isArray(response.body)
? response.body.map(fromProtocolCallHierarchyItem)
: fromProtocolCallHierarchyItem(response.body);
}
public async provideCallHierarchyIncomingCalls(item: vscode.CallHierarchyItem, token: vscode.CancellationToken): Promise<vscode.CallHierarchyIncomingCall[] | undefined> {
const filepath = this.client.toPath(item.uri);
if (!filepath) {
return undefined;
}
const args = typeConverters.Position.toFileLocationRequestArgs(filepath, item.selectionRange.start);
const response = await this.client.execute('provideCallHierarchyIncomingCalls', args, token);
if (response.type !== 'response' || !response.body) {
return undefined;
}
return response.body.map(fromProtocolCallHierchyIncomingCall);
}
public async provideCallHierarchyOutgoingCalls(item: vscode.CallHierarchyItem, token: vscode.CancellationToken): Promise<vscode.CallHierarchyOutgoingCall[] | undefined> {
const filepath = this.client.toPath(item.uri);
if (!filepath) {
return undefined;
}
const args = typeConverters.Position.toFileLocationRequestArgs(filepath, item.selectionRange.start);
const response = await this.client.execute('provideCallHierarchyOutgoingCalls', args, token);
if (response.type !== 'response' || !response.body) {
return undefined;
}
return response.body.map(fromProtocolCallHierchyOutgoingCall);
}
}
function isSourceFileItem(item: Proto.CallHierarchyItem) {
return item.kind === PConst.Kind.script || item.kind === PConst.Kind.module && item.selectionSpan.start.line === 1 && item.selectionSpan.start.offset === 1;
}
function fromProtocolCallHierarchyItem(item: Proto.CallHierarchyItem): vscode.CallHierarchyItem {
const useFileName = isSourceFileItem(item);
const name = useFileName ? path.basename(item.file) : item.name;
const detail = useFileName ? vscode.workspace.asRelativePath(path.dirname(item.file)) : item.containerName ?? '';
const result = new vscode.CallHierarchyItem(
typeConverters.SymbolKind.fromProtocolScriptElementKind(item.kind),
name,
detail,
vscode.Uri.file(item.file),
typeConverters.Range.fromTextSpan(item.span),
typeConverters.Range.fromTextSpan(item.selectionSpan)
);
const kindModifiers = item.kindModifiers ? parseKindModifier(item.kindModifiers) : undefined;
if (kindModifiers?.has(PConst.KindModifiers.depreacted)) {
result.tags = [vscode.SymbolTag.Deprecated];
}
return result;
}
function fromProtocolCallHierchyIncomingCall(item: Proto.CallHierarchyIncomingCall): vscode.CallHierarchyIncomingCall {
return new vscode.CallHierarchyIncomingCall(
fromProtocolCallHierarchyItem(item.from),
item.fromSpans.map(typeConverters.Range.fromTextSpan)
);
}
function fromProtocolCallHierchyOutgoingCall(item: Proto.CallHierarchyOutgoingCall): vscode.CallHierarchyOutgoingCall {
return new vscode.CallHierarchyOutgoingCall(
fromProtocolCallHierarchyItem(item.to),
item.fromSpans.map(typeConverters.Range.fromTextSpan)
);
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient
) {
return conditionalRegistration([
requireMinVersion(client, TypeScriptCallHierarchySupport.minVersion),
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
return vscode.languages.registerCallHierarchyProvider(selector.semantic,
new TypeScriptCallHierarchySupport(client));
});
}

View File

@@ -0,0 +1,119 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import type * as Proto from '../../protocol';
import { CachedResponse } from '../../tsServer/cachedResponse';
import { ITypeScriptServiceClient } from '../../typescriptService';
import { escapeRegExp } from '../../utils/regexp';
import * as typeConverters from '../../utils/typeConverters';
const localize = nls.loadMessageBundle();
export class ReferencesCodeLens extends vscode.CodeLens {
constructor(
public document: vscode.Uri,
public file: string,
range: vscode.Range
) {
super(range);
}
}
export abstract class TypeScriptBaseCodeLensProvider implements vscode.CodeLensProvider {
public static readonly cancelledCommand: vscode.Command = {
// Cancellation is not an error. Just show nothing until we can properly re-compute the code lens
title: '',
command: ''
};
public static readonly errorCommand: vscode.Command = {
title: localize('referenceErrorLabel', 'Could not determine references'),
command: ''
};
private onDidChangeCodeLensesEmitter = new vscode.EventEmitter<void>();
public constructor(
protected client: ITypeScriptServiceClient,
private cachedResponse: CachedResponse<Proto.NavTreeResponse>
) { }
public get onDidChangeCodeLenses(): vscode.Event<void> {
return this.onDidChangeCodeLensesEmitter.event;
}
async provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): Promise<vscode.CodeLens[]> {
const filepath = this.client.toOpenedFilePath(document);
if (!filepath) {
return [];
}
const response = await this.cachedResponse.execute(document, () => this.client.execute('navtree', { file: filepath }, token));
if (response.type !== 'response') {
return [];
}
const tree = response.body;
const referenceableSpans: vscode.Range[] = [];
if (tree && tree.childItems) {
tree.childItems.forEach(item => this.walkNavTree(document, item, null, referenceableSpans));
}
return referenceableSpans.map(span => new ReferencesCodeLens(document.uri, filepath, span));
}
protected abstract extractSymbol(
document: vscode.TextDocument,
item: Proto.NavigationTree,
parent: Proto.NavigationTree | null
): vscode.Range | null;
private walkNavTree(
document: vscode.TextDocument,
item: Proto.NavigationTree,
parent: Proto.NavigationTree | null,
results: vscode.Range[]
): void {
if (!item) {
return;
}
const range = this.extractSymbol(document, item, parent);
if (range) {
results.push(range);
}
(item.childItems || []).forEach(child => this.walkNavTree(document, child, item, results));
}
}
export function getSymbolRange(
document: vscode.TextDocument,
item: Proto.NavigationTree
): vscode.Range | null {
// TS 3.0+ provides a span for just the symbol
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;
}
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(new vscode.Position(range.start.line, range.start.character)) + prefixLength;
return new vscode.Range(
document.positionAt(startOffset),
document.positionAt(startOffset + item.text.length));
}

View File

@@ -0,0 +1,105 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import type * as Proto from '../../protocol';
import * as PConst from '../../protocol.const';
import { CachedResponse } from '../../tsServer/cachedResponse';
import { ClientCapability, ITypeScriptServiceClient } from '../../typescriptService';
import { conditionalRegistration, requireSomeCapability, requireConfiguration } from '../../utils/dependentRegistration';
import { DocumentSelector } from '../../utils/documentSelector';
import * as typeConverters from '../../utils/typeConverters';
import { getSymbolRange, ReferencesCodeLens, TypeScriptBaseCodeLensProvider } from './baseCodeLensProvider';
const localize = nls.loadMessageBundle();
export default class TypeScriptImplementationsCodeLensProvider extends TypeScriptBaseCodeLensProvider {
public async resolveCodeLens(
inputCodeLens: vscode.CodeLens,
token: vscode.CancellationToken,
): Promise<vscode.CodeLens> {
const codeLens = inputCodeLens as ReferencesCodeLens;
const args = typeConverters.Position.toFileLocationRequestArgs(codeLens.file, codeLens.range.start);
const response = await this.client.execute('implementation', args, token, { lowPriority: true, cancelOnResourceChange: codeLens.document });
if (response.type !== 'response' || !response.body) {
codeLens.command = response.type === 'cancelled'
? TypeScriptBaseCodeLensProvider.cancelledCommand
: TypeScriptBaseCodeLensProvider.errorCommand;
return codeLens;
}
const locations = response.body
.map(reference =>
// Only take first line on implementation: https://github.com/microsoft/vscode/issues/23924
new vscode.Location(this.client.toResource(reference.file),
reference.start.line === reference.end.line
? typeConverters.Range.fromTextSpan(reference)
: new vscode.Range(
typeConverters.Position.fromLocation(reference.start),
new vscode.Position(reference.start.line, 0))))
// Exclude original from implementations
.filter(location =>
!(location.uri.toString() === codeLens.document.toString() &&
location.range.start.line === codeLens.range.start.line &&
location.range.start.character === codeLens.range.start.character));
codeLens.command = this.getCommand(locations, codeLens);
return codeLens;
}
private getCommand(locations: vscode.Location[], codeLens: ReferencesCodeLens): vscode.Command | undefined {
return {
title: this.getTitle(locations),
command: locations.length ? 'editor.action.showReferences' : '',
arguments: [codeLens.document, codeLens.range.start, locations]
};
}
private getTitle(locations: vscode.Location[]): string {
return locations.length === 1
? localize('oneImplementationLabel', '1 implementation')
: localize('manyImplementationLabel', '{0} implementations', locations.length);
}
protected extractSymbol(
document: vscode.TextDocument,
item: Proto.NavigationTree,
_parent: Proto.NavigationTree | null
): vscode.Range | null {
switch (item.kind) {
case PConst.Kind.interface:
return getSymbolRange(document, item);
case PConst.Kind.class:
case PConst.Kind.method:
case PConst.Kind.memberVariable:
case PConst.Kind.memberGetAccessor:
case PConst.Kind.memberSetAccessor:
if (item.kindModifiers.match(/\babstract\b/g)) {
return getSymbolRange(document, item);
}
break;
}
return null;
}
}
export function register(
selector: DocumentSelector,
modeId: string,
client: ITypeScriptServiceClient,
cachedResponse: CachedResponse<Proto.NavTreeResponse>,
) {
return conditionalRegistration([
requireConfiguration(modeId, 'implementationsCodeLens.enabled'),
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
return vscode.languages.registerCodeLensProvider(selector.semantic,
new TypeScriptImplementationsCodeLensProvider(client, cachedResponse));
});
}

View File

@@ -0,0 +1,143 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import type * as Proto from '../../protocol';
import * as PConst from '../../protocol.const';
import { CachedResponse } from '../../tsServer/cachedResponse';
import { ExectuionTarget } from '../../tsServer/server';
import { ClientCapability, ITypeScriptServiceClient } from '../../typescriptService';
import { conditionalRegistration, requireConfiguration, requireSomeCapability } from '../../utils/dependentRegistration';
import { DocumentSelector } from '../../utils/documentSelector';
import * as typeConverters from '../../utils/typeConverters';
import { getSymbolRange, ReferencesCodeLens, TypeScriptBaseCodeLensProvider } from './baseCodeLensProvider';
const localize = nls.loadMessageBundle();
export class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLensProvider {
public constructor(
protected client: ITypeScriptServiceClient,
protected _cachedResponse: CachedResponse<Proto.NavTreeResponse>,
private modeId: string
) {
super(client, _cachedResponse);
}
public async resolveCodeLens(inputCodeLens: vscode.CodeLens, token: vscode.CancellationToken): Promise<vscode.CodeLens> {
const codeLens = inputCodeLens as ReferencesCodeLens;
const args = typeConverters.Position.toFileLocationRequestArgs(codeLens.file, codeLens.range.start);
const response = await this.client.execute('references', args, token, {
lowPriority: true,
executionTarget: ExectuionTarget.Semantic,
cancelOnResourceChange: codeLens.document,
});
if (response.type !== 'response' || !response.body) {
codeLens.command = response.type === 'cancelled'
? TypeScriptBaseCodeLensProvider.cancelledCommand
: TypeScriptBaseCodeLensProvider.errorCommand;
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() === codeLens.document.toString() &&
location.range.start.isEqual(codeLens.range.start)));
codeLens.command = {
title: this.getCodeLensLabel(locations),
command: locations.length ? 'editor.action.showReferences' : '',
arguments: [codeLens.document, codeLens.range.start, locations]
};
return codeLens;
}
private getCodeLensLabel(locations: ReadonlyArray<vscode.Location>): string {
return locations.length === 1
? localize('oneReferenceLabel', '1 reference')
: localize('manyReferenceLabel', '{0} references', locations.length);
}
protected extractSymbol(
document: vscode.TextDocument,
item: Proto.NavigationTree,
parent: Proto.NavigationTree | null
): vscode.Range | null {
if (parent && parent.kind === PConst.Kind.enum) {
return getSymbolRange(document, item);
}
switch (item.kind) {
case PConst.Kind.function:
const showOnAllFunctions = vscode.workspace.getConfiguration(this.modeId).get<boolean>('referencesCodeLens.showOnAllFunctions');
if (showOnAllFunctions) {
return getSymbolRange(document, item);
}
// fallthrough
case PConst.Kind.const:
case PConst.Kind.let:
case PConst.Kind.variable:
// Only show references for exported variables
if (/\bexport\b/.test(item.kindModifiers)) {
return getSymbolRange(document, item);
}
break;
case PConst.Kind.class:
if (item.text === '<class>') {
break;
}
return getSymbolRange(document, item);
case PConst.Kind.interface:
case PConst.Kind.type:
case PConst.Kind.enum:
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 &&
typeConverters.Position.fromLocation(parent.spans[0].start).isEqual(typeConverters.Position.fromLocation(item.spans[0].start))
) {
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 register(
selector: DocumentSelector,
modeId: string,
client: ITypeScriptServiceClient,
cachedResponse: CachedResponse<Proto.NavTreeResponse>,
) {
return conditionalRegistration([
requireConfiguration(modeId, 'referencesCodeLens.enabled'),
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
return vscode.languages.registerCodeLensProvider(selector.semantic,
new TypeScriptReferencesCodeLensProvider(client, cachedResponse, modeId));
});
}

View File

@@ -0,0 +1,841 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { Command, CommandManager } from '../commands/commandManager';
import type * as Proto from '../protocol';
import * as PConst from '../protocol.const';
import { ClientCapability, ITypeScriptServiceClient, ServerResponse } from '../typescriptService';
import API from '../utils/api';
import { nulToken } from '../utils/cancellation';
import { applyCodeAction } from '../utils/codeAction';
import { conditionalRegistration, requireConfiguration, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import { parseKindModifier } from '../utils/modifiers';
import * as Previewer from '../utils/previewer';
import { snippetForFunctionCall } from '../utils/snippetForFunctionCall';
import { TelemetryReporter } from '../utils/telemetry';
import * as typeConverters from '../utils/typeConverters';
import TypingsStatus from '../utils/typingsStatus';
import FileConfigurationManager from './fileConfigurationManager';
const localize = nls.loadMessageBundle();
interface DotAccessorContext {
readonly range: vscode.Range;
readonly text: string;
}
interface CompletionContext {
readonly isNewIdentifierLocation: boolean;
readonly isMemberCompletion: boolean;
readonly isInValidCommitCharacterContext: boolean;
readonly dotAccessorContext?: DotAccessorContext;
readonly enableCallCompletions: boolean;
readonly useCodeSnippetsOnMethodSuggest: boolean,
readonly wordRange: vscode.Range | undefined;
readonly line: string;
readonly useFuzzyWordRangeLogic: boolean,
}
class MyCompletionItem extends vscode.CompletionItem {
public readonly useCodeSnippet: boolean;
constructor(
public readonly position: vscode.Position,
public readonly document: vscode.TextDocument,
public readonly tsEntry: Proto.CompletionEntry,
private readonly completionContext: CompletionContext,
public readonly metadata: any | undefined,
) {
super(tsEntry.name, MyCompletionItem.convertKind(tsEntry.kind));
if (tsEntry.source) {
// De-prioritze auto-imports
// https://github.com/microsoft/vscode/issues/40311
this.sortText = '\uffff' + tsEntry.sortText;
} else {
this.sortText = tsEntry.sortText;
}
this.preselect = tsEntry.isRecommended;
this.position = position;
this.useCodeSnippet = completionContext.useCodeSnippetsOnMethodSuggest && (this.kind === vscode.CompletionItemKind.Function || this.kind === vscode.CompletionItemKind.Method);
this.range = this.getRangeFromReplacementSpan(tsEntry, completionContext, position);
this.commitCharacters = MyCompletionItem.getCommitCharacters(completionContext, tsEntry);
this.insertText = tsEntry.insertText;
this.filterText = this.getFilterText(completionContext.line, tsEntry.insertText);
if (completionContext.isMemberCompletion && completionContext.dotAccessorContext) {
this.filterText = completionContext.dotAccessorContext.text + (this.insertText || this.label);
if (!this.range) {
const replacementRange = this.getFuzzyWordRange();
if (replacementRange) {
this.range = {
inserting: completionContext.dotAccessorContext.range,
replacing: completionContext.dotAccessorContext.range.union(replacementRange),
};
} else {
this.range = completionContext.dotAccessorContext.range;
}
this.insertText = this.filterText;
}
}
if (tsEntry.kindModifiers) {
const kindModifiers = parseKindModifier(tsEntry.kindModifiers);
if (kindModifiers.has(PConst.KindModifiers.optional)) {
if (!this.insertText) {
this.insertText = this.label;
}
if (!this.filterText) {
this.filterText = this.label;
}
this.label += '?';
}
if (kindModifiers.has(PConst.KindModifiers.depreacted)) {
this.tags = [vscode.CompletionItemTag.Deprecated];
}
if (kindModifiers.has(PConst.KindModifiers.color)) {
this.kind = vscode.CompletionItemKind.Color;
}
if (tsEntry.kind === PConst.Kind.script) {
for (const extModifier of PConst.KindModifiers.fileExtensionKindModifiers) {
if (kindModifiers.has(extModifier)) {
if (tsEntry.name.toLowerCase().endsWith(extModifier)) {
this.detail = tsEntry.name;
} else {
this.detail = tsEntry.name + extModifier;
}
break;
}
}
}
}
this.resolveRange();
}
private getRangeFromReplacementSpan(tsEntry: Proto.CompletionEntry, completionContext: CompletionContext, position: vscode.Position) {
if (!tsEntry.replacementSpan) {
return;
}
let replaceRange = typeConverters.Range.fromTextSpan(tsEntry.replacementSpan);
// Make sure we only replace a single line at most
if (!replaceRange.isSingleLine) {
replaceRange = new vscode.Range(replaceRange.start.line, replaceRange.start.character, replaceRange.start.line, completionContext.line.length);
}
return {
inserting: new vscode.Range(replaceRange.start, position),
replacing: replaceRange,
};
}
private getFilterText(line: string, insertText: string | undefined): string | undefined {
// Handle private field completions
if (this.tsEntry.name.startsWith('#')) {
const wordRange = this.completionContext.wordRange;
const wordStart = wordRange ? line.charAt(wordRange.start.character) : undefined;
if (insertText) {
if (insertText.startsWith('this.#')) {
return wordStart === '#' ? insertText : insertText.replace(/^this\.#/, '');
} else {
return insertText;
}
} else {
return wordStart === '#' ? undefined : this.tsEntry.name.replace(/^#/, '');
}
}
// For `this.` completions, generally don't set the filter text since we don't want them to be overly prioritized. #74164
if (insertText?.startsWith('this.')) {
return undefined;
}
// Handle the case:
// ```
// const xyz = { 'ab c': 1 };
// xyz.ab|
// ```
// In which case we want to insert a bracket accessor but should use `.abc` as the filter text instead of
// the bracketed insert text.
else if (insertText?.startsWith('[')) {
return insertText.replace(/^\[['"](.+)[['"]\]$/, '.$1');
}
// In all other cases, fallback to using the insertText
return insertText;
}
private resolveRange(): void {
if (this.range) {
return;
}
const replaceRange = this.getFuzzyWordRange();
if (replaceRange) {
this.range = {
inserting: new vscode.Range(replaceRange.start, this.position),
replacing: replaceRange
};
}
}
private getFuzzyWordRange() {
if (this.completionContext.useFuzzyWordRangeLogic) {
// Try getting longer, prefix based range for completions that span words
const text = this.completionContext.line.slice(Math.max(0, this.position.character - this.label.length), this.position.character).toLowerCase();
const entryName = this.label.toLowerCase();
for (let i = entryName.length; i >= 0; --i) {
if (text.endsWith(entryName.substr(0, i)) && (!this.completionContext.wordRange || this.completionContext.wordRange.start.character > this.position.character - i)) {
return new vscode.Range(
new vscode.Position(this.position.line, Math.max(0, this.position.character - i)),
this.position);
}
}
}
return this.completionContext.wordRange;
}
private static convertKind(kind: string): vscode.CompletionItemKind {
switch (kind) {
case PConst.Kind.primitiveType:
case PConst.Kind.keyword:
return vscode.CompletionItemKind.Keyword;
case PConst.Kind.const:
case PConst.Kind.let:
case PConst.Kind.variable:
case PConst.Kind.localVariable:
case PConst.Kind.alias:
case PConst.Kind.parameter:
return vscode.CompletionItemKind.Variable;
case PConst.Kind.memberVariable:
case PConst.Kind.memberGetAccessor:
case PConst.Kind.memberSetAccessor:
return vscode.CompletionItemKind.Field;
case PConst.Kind.function:
case PConst.Kind.localFunction:
return vscode.CompletionItemKind.Function;
case PConst.Kind.method:
case PConst.Kind.constructSignature:
case PConst.Kind.callSignature:
case PConst.Kind.indexSignature:
return vscode.CompletionItemKind.Method;
case PConst.Kind.enum:
return vscode.CompletionItemKind.Enum;
case PConst.Kind.enumMember:
return vscode.CompletionItemKind.EnumMember;
case PConst.Kind.module:
case PConst.Kind.externalModuleName:
return vscode.CompletionItemKind.Module;
case PConst.Kind.class:
case PConst.Kind.type:
return vscode.CompletionItemKind.Class;
case PConst.Kind.interface:
return vscode.CompletionItemKind.Interface;
case PConst.Kind.warning:
return vscode.CompletionItemKind.Text;
case PConst.Kind.script:
return vscode.CompletionItemKind.File;
case PConst.Kind.directory:
return vscode.CompletionItemKind.Folder;
case PConst.Kind.string:
return vscode.CompletionItemKind.Constant;
default:
return vscode.CompletionItemKind.Property;
}
}
private static getCommitCharacters(context: CompletionContext, entry: Proto.CompletionEntry): string[] | undefined {
if (context.isNewIdentifierLocation || !context.isInValidCommitCharacterContext) {
return undefined;
}
const commitCharacters: string[] = [];
switch (entry.kind) {
case PConst.Kind.memberGetAccessor:
case PConst.Kind.memberSetAccessor:
case PConst.Kind.constructSignature:
case PConst.Kind.callSignature:
case PConst.Kind.indexSignature:
case PConst.Kind.enum:
case PConst.Kind.interface:
commitCharacters.push('.', ';');
break;
case PConst.Kind.module:
case PConst.Kind.alias:
case PConst.Kind.const:
case PConst.Kind.let:
case PConst.Kind.variable:
case PConst.Kind.localVariable:
case PConst.Kind.memberVariable:
case PConst.Kind.class:
case PConst.Kind.function:
case PConst.Kind.method:
case PConst.Kind.keyword:
case PConst.Kind.parameter:
commitCharacters.push('.', ',', ';');
if (context.enableCallCompletions) {
commitCharacters.push('(');
}
break;
}
return commitCharacters.length === 0 ? undefined : commitCharacters;
}
}
class CompositeCommand implements Command {
public static readonly ID = '_typescript.composite';
public readonly id = CompositeCommand.ID;
public execute(...commands: vscode.Command[]) {
for (const command of commands) {
vscode.commands.executeCommand(command.command, ...(command.arguments || []));
}
}
}
class CompletionAcceptedCommand implements Command {
public static readonly ID = '_typescript.onCompletionAccepted';
public readonly id = CompletionAcceptedCommand.ID;
public constructor(
private readonly onCompletionAccepted: (item: vscode.CompletionItem) => void,
private readonly telemetryReporter: TelemetryReporter,
) { }
public execute(item: vscode.CompletionItem) {
this.onCompletionAccepted(item);
if (item instanceof MyCompletionItem) {
/* __GDPR__
"completions.accept" : {
"isPackageJsonImport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"${include}": [
"${TypeScriptCommonProperties}"
]
}
*/
this.telemetryReporter.logTelemetry('completions.accept', {
isPackageJsonImport: item.tsEntry.isPackageJsonImport ? 'true' : undefined,
});
}
}
}
class ApplyCompletionCodeActionCommand implements Command {
public static readonly ID = '_typescript.applyCompletionCodeAction';
public readonly id = ApplyCompletionCodeActionCommand.ID;
public constructor(
private readonly client: ITypeScriptServiceClient
) { }
public async execute(_file: string, codeActions: Proto.CodeAction[]): Promise<boolean> {
if (codeActions.length === 0) {
return true;
}
if (codeActions.length === 1) {
return applyCodeAction(this.client, codeActions[0], nulToken);
}
const selection = await vscode.window.showQuickPick(
codeActions.map(action => ({
label: action.description,
description: '',
action,
})), {
placeHolder: localize('selectCodeAction', 'Select code action to apply')
});
if (selection) {
return applyCodeAction(this.client, selection.action, nulToken);
}
return false;
}
}
interface CompletionConfiguration {
readonly useCodeSnippetsOnMethodSuggest: boolean;
readonly nameSuggestions: boolean;
readonly pathSuggestions: boolean;
readonly autoImportSuggestions: boolean;
}
namespace CompletionConfiguration {
export const useCodeSnippetsOnMethodSuggest = 'suggest.completeFunctionCalls';
export const nameSuggestions = 'suggest.names';
export const pathSuggestions = 'suggest.paths';
export const autoImportSuggestions = 'suggest.autoImports';
export function getConfigurationForResource(
modeId: string,
resource: vscode.Uri
): CompletionConfiguration {
const config = vscode.workspace.getConfiguration(modeId, resource);
return {
useCodeSnippetsOnMethodSuggest: config.get<boolean>(CompletionConfiguration.useCodeSnippetsOnMethodSuggest, false),
pathSuggestions: config.get<boolean>(CompletionConfiguration.pathSuggestions, true),
autoImportSuggestions: config.get<boolean>(CompletionConfiguration.autoImportSuggestions, true),
nameSuggestions: config.get<boolean>(CompletionConfiguration.nameSuggestions, true),
};
}
}
class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider<MyCompletionItem> {
public static readonly triggerCharacters = ['.', '"', '\'', '`', '/', '@', '<', '#'];
constructor(
private readonly client: ITypeScriptServiceClient,
private readonly modeId: string,
private readonly typingsStatus: TypingsStatus,
private readonly fileConfigurationManager: FileConfigurationManager,
commandManager: CommandManager,
private readonly telemetryReporter: TelemetryReporter,
onCompletionAccepted: (item: vscode.CompletionItem) => void
) {
commandManager.register(new ApplyCompletionCodeActionCommand(this.client));
commandManager.register(new CompositeCommand());
commandManager.register(new CompletionAcceptedCommand(onCompletionAccepted, this.telemetryReporter));
}
public async provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken,
context: vscode.CompletionContext
): Promise<vscode.CompletionList<MyCompletionItem> | undefined> {
if (this.typingsStatus.isAcquiringTypings) {
return Promise.reject<vscode.CompletionList<MyCompletionItem>>({
label: localize(
{ key: 'acquiringTypingsLabel', comment: ['Typings refers to the *.d.ts typings files that power our IntelliSense. It should not be localized'] },
'Acquiring typings...'),
detail: localize(
{ key: 'acquiringTypingsDetail', comment: ['Typings refers to the *.d.ts typings files that power our IntelliSense. It should not be localized'] },
'Acquiring typings definitions for IntelliSense.')
});
}
const file = this.client.toOpenedFilePath(document);
if (!file) {
return undefined;
}
const line = document.lineAt(position.line);
const completionConfiguration = CompletionConfiguration.getConfigurationForResource(this.modeId, document.uri);
if (!this.shouldTrigger(context, line, position)) {
return undefined;
}
const wordRange = document.getWordRangeAtPosition(position);
await this.client.interruptGetErr(() => this.fileConfigurationManager.ensureConfigurationForDocument(document, token));
const args: Proto.CompletionsRequestArgs = {
...typeConverters.Position.toFileLocationRequestArgs(file, position),
includeExternalModuleExports: completionConfiguration.autoImportSuggestions,
includeInsertTextCompletions: true,
triggerCharacter: this.getTsTriggerCharacter(context),
};
let isNewIdentifierLocation = true;
let isIncomplete = false;
let isMemberCompletion = false;
let dotAccessorContext: DotAccessorContext | undefined;
let entries: ReadonlyArray<Proto.CompletionEntry>;
let metadata: any | undefined;
let response: ServerResponse.Response<Proto.CompletionInfoResponse> | undefined;
let duration: number | undefined;
if (this.client.apiVersion.gte(API.v300)) {
const startTime = Date.now();
try {
response = await this.client.interruptGetErr(() => this.client.execute('completionInfo', args, token));
} finally {
duration = Date.now() - startTime;
}
if (response.type !== 'response' || !response.body) {
this.logCompletionsTelemetry(duration, response);
return undefined;
}
isNewIdentifierLocation = response.body.isNewIdentifierLocation;
isMemberCompletion = response.body.isMemberCompletion;
if (isMemberCompletion) {
const dotMatch = line.text.slice(0, position.character).match(/\??\.\s*$/) || undefined;
if (dotMatch) {
const range = new vscode.Range(position.translate({ characterDelta: -dotMatch[0].length }), position);
const text = document.getText(range);
dotAccessorContext = { range, text };
}
}
isIncomplete = (response as any).metadata && (response as any).metadata.isIncomplete;
entries = response.body.entries;
metadata = response.metadata;
} else {
const response = await this.client.interruptGetErr(() => this.client.execute('completions', args, token));
if (response.type !== 'response' || !response.body) {
return undefined;
}
entries = response.body;
metadata = response.metadata;
}
const completionContext = {
isNewIdentifierLocation,
isMemberCompletion,
dotAccessorContext,
isInValidCommitCharacterContext: this.isInValidCommitCharacterContext(document, position),
enableCallCompletions: !completionConfiguration.useCodeSnippetsOnMethodSuggest,
wordRange,
line: line.text,
useCodeSnippetsOnMethodSuggest: completionConfiguration.useCodeSnippetsOnMethodSuggest,
useFuzzyWordRangeLogic: this.client.apiVersion.lt(API.v390),
};
let includesPackageJsonImport = false;
const items: MyCompletionItem[] = [];
for (let entry of entries) {
if (!shouldExcludeCompletionEntry(entry, completionConfiguration)) {
items.push(new MyCompletionItem(position, document, entry, completionContext, metadata));
includesPackageJsonImport = !!entry.isPackageJsonImport;
}
}
if (duration !== undefined) {
this.logCompletionsTelemetry(duration, response, includesPackageJsonImport);
}
return new vscode.CompletionList(items, isIncomplete);
}
private logCompletionsTelemetry(
duration: number,
response: ServerResponse.Response<Proto.CompletionInfoResponse> | undefined,
includesPackageJsonImport?: boolean
) {
/* __GDPR__
"completions.execute" : {
"duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"type" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"updateGraphDurationMs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"createAutoImportProviderProgramDurationMs" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"includesPackageJsonImport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"${include}": [
"${TypeScriptCommonProperties}"
]
}
*/
this.telemetryReporter.logTelemetry('completions.execute', {
duration: duration,
type: response?.type ?? 'unknown',
count: response?.type === 'response' && response.body ? response.body.entries.length : 0,
updateGraphDurationMs: response?.type === 'response' ? response.performanceData?.updateGraphDurationMs : undefined,
createAutoImportProviderProgramDurationMs: response?.type === 'response' ? (response.performanceData as Proto.PerformanceData & { createAutoImportProviderProgramDurationMs?: number })?.createAutoImportProviderProgramDurationMs : undefined,
includesPackageJsonImport: includesPackageJsonImport ? 'true' : undefined,
});
}
private getTsTriggerCharacter(context: vscode.CompletionContext): Proto.CompletionsTriggerCharacter | undefined {
switch (context.triggerCharacter) {
case '@': // Workaround for https://github.com/microsoft/TypeScript/issues/27321
return this.client.apiVersion.gte(API.v310) && this.client.apiVersion.lt(API.v320) ? undefined : '@';
case '#': // Workaround for https://github.com/microsoft/TypeScript/issues/36367
return this.client.apiVersion.lt(API.v381) ? undefined : '#';
case '.':
case '"':
case '\'':
case '`':
case '/':
case '<':
return context.triggerCharacter;
}
return undefined;
}
public async resolveCompletionItem(
item: MyCompletionItem,
token: vscode.CancellationToken
): Promise<MyCompletionItem | undefined> {
const filepath = this.client.toOpenedFilePath(item.document);
if (!filepath) {
return undefined;
}
const args: Proto.CompletionDetailsRequestArgs = {
...typeConverters.Position.toFileLocationRequestArgs(filepath, item.position),
entryNames: [
item.tsEntry.source ? { name: item.tsEntry.name, source: item.tsEntry.source } : item.tsEntry.name
]
};
const response = await this.client.interruptGetErr(() => this.client.execute('completionEntryDetails', args, token));
if (response.type !== 'response' || !response.body || !response.body.length) {
return item;
}
const detail = response.body[0];
if (!item.detail && detail.displayParts.length) {
item.detail = Previewer.plain(detail.displayParts);
}
item.documentation = this.getDocumentation(detail, item);
const codeAction = this.getCodeActions(detail, filepath);
const commands: vscode.Command[] = [{
command: CompletionAcceptedCommand.ID,
title: '',
arguments: [item]
}];
if (codeAction.command) {
commands.push(codeAction.command);
}
item.additionalTextEdits = codeAction.additionalTextEdits;
if (item.useCodeSnippet) {
const shouldCompleteFunction = await this.isValidFunctionCompletionContext(filepath, item.position, item.document, token);
if (shouldCompleteFunction) {
const { snippet, parameterCount } = snippetForFunctionCall(item, detail.displayParts);
item.insertText = snippet;
if (parameterCount > 0) {
//Fix for https://github.com/microsoft/vscode/issues/104059
//Don't show parameter hints if "editor.parameterHints.enabled": false
if (vscode.workspace.getConfiguration('editor.parameterHints').get('enabled')) {
commands.push({ title: 'triggerParameterHints', command: 'editor.action.triggerParameterHints' });
}
}
}
}
if (commands.length) {
if (commands.length === 1) {
item.command = commands[0];
} else {
item.command = {
command: CompositeCommand.ID,
title: '',
arguments: commands
};
}
}
return item;
}
private getCodeActions(
detail: Proto.CompletionEntryDetails,
filepath: string
): { command?: vscode.Command, additionalTextEdits?: vscode.TextEdit[] } {
if (!detail.codeActions || !detail.codeActions.length) {
return {};
}
// Try to extract out the additionalTextEdits for the current file.
// Also check if we still have to apply other workspace edits and commands
// using a vscode command
const additionalTextEdits: vscode.TextEdit[] = [];
let hasReaminingCommandsOrEdits = false;
for (const tsAction of detail.codeActions) {
if (tsAction.commands) {
hasReaminingCommandsOrEdits = true;
}
// Apply all edits in the current file using `additionalTextEdits`
if (tsAction.changes) {
for (const change of tsAction.changes) {
if (change.fileName === filepath) {
additionalTextEdits.push(...change.textChanges.map(typeConverters.TextEdit.fromCodeEdit));
} else {
hasReaminingCommandsOrEdits = true;
}
}
}
}
let command: vscode.Command | undefined = undefined;
if (hasReaminingCommandsOrEdits) {
// Create command that applies all edits not in the current file.
command = {
title: '',
command: ApplyCompletionCodeActionCommand.ID,
arguments: [filepath, detail.codeActions.map((x): Proto.CodeAction => ({
commands: x.commands,
description: x.description,
changes: x.changes.filter(x => x.fileName !== filepath)
}))]
};
}
return {
command,
additionalTextEdits: additionalTextEdits.length ? additionalTextEdits : undefined
};
}
private isInValidCommitCharacterContext(
document: vscode.TextDocument,
position: vscode.Position
): boolean {
if (this.client.apiVersion.lt(API.v320)) {
// Workaround for https://github.com/microsoft/TypeScript/issues/27742
// Only enable dot completions when previous character not a dot preceded by whitespace.
// Prevents incorrectly completing while typing spread operators.
if (position.character > 1) {
const preText = document.getText(new vscode.Range(
position.line, 0,
position.line, position.character));
return preText.match(/(\s|^)\.$/ig) === null;
}
}
return true;
}
private shouldTrigger(
context: vscode.CompletionContext,
line: vscode.TextLine,
position: vscode.Position
): boolean {
if (context.triggerCharacter && this.client.apiVersion.lt(API.v290)) {
if ((context.triggerCharacter === '"' || context.triggerCharacter === '\'')) {
// make sure we are in something that looks like the start of an import
const pre = line.text.slice(0, position.character);
if (!pre.match(/\b(from|import)\s*["']$/) && !pre.match(/\b(import|require)\(['"]$/)) {
return false;
}
}
if (context.triggerCharacter === '/') {
// make sure we are in something that looks like an import path
const pre = line.text.slice(0, position.character);
if (!pre.match(/\b(from|import)\s*["'][^'"]*$/) && !pre.match(/\b(import|require)\(['"][^'"]*$/)) {
return false;
}
}
if (context.triggerCharacter === '@') {
// make sure we are in something that looks like the start of a jsdoc comment
const pre = line.text.slice(0, position.character);
if (!pre.match(/^\s*\*[ ]?@/) && !pre.match(/\/\*\*+[ ]?@/)) {
return false;
}
}
if (context.triggerCharacter === '<') {
return false;
}
}
return true;
}
private getDocumentation(
detail: Proto.CompletionEntryDetails,
item: MyCompletionItem
): vscode.MarkdownString | undefined {
const documentation = new vscode.MarkdownString();
if (detail.source) {
const importPath = `'${Previewer.plain(detail.source)}'`;
const autoImportLabel = localize('autoImportLabel', 'Auto import from {0}', importPath);
item.detail = `${autoImportLabel}\n${item.detail}`;
}
Previewer.addMarkdownDocumentation(documentation, detail.documentation, detail.tags);
return documentation.value.length ? documentation : undefined;
}
private async isValidFunctionCompletionContext(
filepath: string,
position: vscode.Position,
document: vscode.TextDocument,
token: vscode.CancellationToken
): Promise<boolean> {
// Workaround for https://github.com/microsoft/TypeScript/issues/12677
// Don't complete function calls inside of destructive assignments or imports
try {
const args: Proto.FileLocationRequestArgs = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
const response = await this.client.execute('quickinfo', args, token);
if (response.type === 'response' && response.body) {
switch (response.body.kind) {
case 'var':
case 'let':
case 'const':
case 'alias':
return false;
}
}
} catch {
// Noop
}
// Don't complete function call if there is already something that looks like a function call
// https://github.com/microsoft/vscode/issues/18131
const after = document.lineAt(position.line).text.slice(position.character);
return after.match(/^[a-z_$0-9]*\s*\(/gi) === null;
}
}
function shouldExcludeCompletionEntry(
element: Proto.CompletionEntry,
completionConfiguration: CompletionConfiguration
) {
return (
(!completionConfiguration.nameSuggestions && element.kind === PConst.Kind.warning)
|| (!completionConfiguration.pathSuggestions &&
(element.kind === PConst.Kind.directory || element.kind === PConst.Kind.script || element.kind === PConst.Kind.externalModuleName))
|| (!completionConfiguration.autoImportSuggestions && element.hasAction)
);
}
export function register(
selector: DocumentSelector,
modeId: string,
client: ITypeScriptServiceClient,
typingsStatus: TypingsStatus,
fileConfigurationManager: FileConfigurationManager,
commandManager: CommandManager,
telemetryReporter: TelemetryReporter,
onCompletionAccepted: (item: vscode.CompletionItem) => void
) {
return conditionalRegistration([
requireConfiguration(modeId, 'suggest.enabled'),
requireSomeCapability(client, ClientCapability.EnhancedSyntax, ClientCapability.Semantic),
], () => {
return vscode.languages.registerCompletionItemProvider(selector.syntax,
new TypeScriptCompletionItemProvider(client, modeId, typingsStatus, fileConfigurationManager, commandManager, telemetryReporter, onCompletionAccepted),
...TypeScriptCompletionItemProvider.triggerCharacters);
});
}

View File

@@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ITypeScriptServiceClient } from '../typescriptService';
import * as typeConverters from '../utils/typeConverters';
export default class TypeScriptDefinitionProviderBase {
constructor(
protected readonly client: ITypeScriptServiceClient
) { }
protected async getSymbolLocations(
definitionType: 'definition' | 'implementation' | 'typeDefinition',
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.Location[] | undefined> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return undefined;
}
const args = typeConverters.Position.toFileLocationRequestArgs(file, position);
const response = await this.client.execute(definitionType, args, token);
if (response.type !== 'response' || !response.body) {
return undefined;
}
return response.body.map(location =>
typeConverters.Location.fromTextSpan(this.client.toResource(location.file), location));
}
}

View File

@@ -0,0 +1,72 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as typeConverters from '../utils/typeConverters';
import DefinitionProviderBase from './definitionProviderBase';
export default class TypeScriptDefinitionProvider extends DefinitionProviderBase implements vscode.DefinitionProvider {
constructor(
client: ITypeScriptServiceClient
) {
super(client);
}
public async provideDefinition(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.DefinitionLink[] | vscode.Definition | undefined> {
if (this.client.apiVersion.gte(API.v270)) {
const filepath = this.client.toOpenedFilePath(document);
if (!filepath) {
return undefined;
}
const args = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
const response = await this.client.execute('definitionAndBoundSpan', args, token);
if (response.type !== 'response' || !response.body) {
return undefined;
}
const span = response.body.textSpan ? typeConverters.Range.fromTextSpan(response.body.textSpan) : undefined;
return response.body.definitions
.map((location): vscode.DefinitionLink => {
const target = typeConverters.Location.fromTextSpan(this.client.toResource(location.file), location);
if (location.contextStart && location.contextEnd) {
return {
originSelectionRange: span,
targetRange: typeConverters.Range.fromLocations(location.contextStart, location.contextEnd),
targetUri: target.uri,
targetSelectionRange: target.range,
};
}
return {
originSelectionRange: span,
targetRange: target.range,
targetUri: target.uri
};
});
}
return this.getSymbolLocations('definition', document, position, token);
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
) {
return conditionalRegistration([
requireSomeCapability(client, ClientCapability.EnhancedSyntax, ClientCapability.Semantic),
], () => {
return vscode.languages.registerDefinitionProvider(selector.syntax,
new TypeScriptDefinitionProvider(client));
});
}

View File

@@ -0,0 +1,251 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ResourceMap } from '../utils/resourceMap';
import { DiagnosticLanguage } from '../utils/languageDescription';
import * as arrays from '../utils/arrays';
import { Disposable } from '../utils/dispose';
function diagnosticsEquals(a: vscode.Diagnostic, b: vscode.Diagnostic): boolean {
if (a === b) {
return true;
}
return a.code === b.code
&& a.message === b.message
&& a.severity === b.severity
&& a.source === b.source
&& a.range.isEqual(b.range)
&& arrays.equals(a.relatedInformation || arrays.empty, b.relatedInformation || arrays.empty, (a, b) => {
return a.message === b.message
&& a.location.range.isEqual(b.location.range)
&& a.location.uri.fsPath === b.location.uri.fsPath;
})
&& arrays.equals(a.tags || arrays.empty, b.tags || arrays.empty);
}
export const enum DiagnosticKind {
Syntax,
Semantic,
Suggestion,
}
class FileDiagnostics {
private readonly _diagnostics = new Map<DiagnosticKind, ReadonlyArray<vscode.Diagnostic>>();
constructor(
public readonly file: vscode.Uri,
public language: DiagnosticLanguage
) { }
public updateDiagnostics(
language: DiagnosticLanguage,
kind: DiagnosticKind,
diagnostics: ReadonlyArray<vscode.Diagnostic>
): boolean {
if (language !== this.language) {
this._diagnostics.clear();
this.language = language;
}
const existing = this._diagnostics.get(kind);
if (arrays.equals(existing || arrays.empty, diagnostics, diagnosticsEquals)) {
// No need to update
return false;
}
this._diagnostics.set(kind, diagnostics);
return true;
}
public getDiagnostics(settings: DiagnosticSettings): vscode.Diagnostic[] {
if (!settings.getValidate(this.language)) {
return [];
}
return [
...this.get(DiagnosticKind.Syntax),
...this.get(DiagnosticKind.Semantic),
...this.getSuggestionDiagnostics(settings),
];
}
private getSuggestionDiagnostics(settings: DiagnosticSettings) {
const enableSuggestions = settings.getEnableSuggestions(this.language);
return this.get(DiagnosticKind.Suggestion).filter(x => {
if (!enableSuggestions) {
// Still show unused
return x.tags && (x.tags.includes(vscode.DiagnosticTag.Unnecessary) || x.tags.includes(vscode.DiagnosticTag.Deprecated));
}
return true;
});
}
private get(kind: DiagnosticKind): ReadonlyArray<vscode.Diagnostic> {
return this._diagnostics.get(kind) || [];
}
}
interface LanguageDiagnosticSettings {
readonly validate: boolean;
readonly enableSuggestions: boolean;
}
function areLanguageDiagnosticSettingsEqual(currentSettings: LanguageDiagnosticSettings, newSettings: LanguageDiagnosticSettings): boolean {
return currentSettings.validate === newSettings.validate
&& currentSettings.enableSuggestions && currentSettings.enableSuggestions;
}
class DiagnosticSettings {
private static readonly defaultSettings: LanguageDiagnosticSettings = {
validate: true,
enableSuggestions: true
};
private readonly _languageSettings = new Map<DiagnosticLanguage, LanguageDiagnosticSettings>();
public getValidate(language: DiagnosticLanguage): boolean {
return this.get(language).validate;
}
public setValidate(language: DiagnosticLanguage, value: boolean): boolean {
return this.update(language, settings => ({
validate: value,
enableSuggestions: settings.enableSuggestions,
}));
}
public getEnableSuggestions(language: DiagnosticLanguage): boolean {
return this.get(language).enableSuggestions;
}
public setEnableSuggestions(language: DiagnosticLanguage, value: boolean): boolean {
return this.update(language, settings => ({
validate: settings.validate,
enableSuggestions: value
}));
}
private get(language: DiagnosticLanguage): LanguageDiagnosticSettings {
return this._languageSettings.get(language) || DiagnosticSettings.defaultSettings;
}
private update(language: DiagnosticLanguage, f: (x: LanguageDiagnosticSettings) => LanguageDiagnosticSettings): boolean {
const currentSettings = this.get(language);
const newSettings = f(currentSettings);
this._languageSettings.set(language, newSettings);
return areLanguageDiagnosticSettingsEqual(currentSettings, newSettings);
}
}
export class DiagnosticsManager extends Disposable {
private readonly _diagnostics: ResourceMap<FileDiagnostics>;
private readonly _settings = new DiagnosticSettings();
private readonly _currentDiagnostics: vscode.DiagnosticCollection;
private readonly _pendingUpdates: ResourceMap<any>;
private readonly _updateDelay = 50;
constructor(
owner: string,
onCaseInsenitiveFileSystem: boolean
) {
super();
this._diagnostics = new ResourceMap<FileDiagnostics>(undefined, { onCaseInsenitiveFileSystem });
this._pendingUpdates = new ResourceMap<any>(undefined, { onCaseInsenitiveFileSystem });
this._currentDiagnostics = this._register(vscode.languages.createDiagnosticCollection(owner));
}
public dispose() {
super.dispose();
for (const value of this._pendingUpdates.values) {
clearTimeout(value);
}
this._pendingUpdates.clear();
}
public reInitialize(): void {
this._currentDiagnostics.clear();
this._diagnostics.clear();
}
public setValidate(language: DiagnosticLanguage, value: boolean) {
const didUpdate = this._settings.setValidate(language, value);
if (didUpdate) {
this.rebuild();
}
}
public setEnableSuggestions(language: DiagnosticLanguage, value: boolean) {
const didUpdate = this._settings.setEnableSuggestions(language, value);
if (didUpdate) {
this.rebuild();
}
}
public updateDiagnostics(
file: vscode.Uri,
language: DiagnosticLanguage,
kind: DiagnosticKind,
diagnostics: ReadonlyArray<vscode.Diagnostic>
): void {
let didUpdate = false;
const entry = this._diagnostics.get(file);
if (entry) {
didUpdate = entry.updateDiagnostics(language, kind, diagnostics);
} else if (diagnostics.length) {
const fileDiagnostics = new FileDiagnostics(file, language);
fileDiagnostics.updateDiagnostics(language, kind, diagnostics);
this._diagnostics.set(file, fileDiagnostics);
didUpdate = true;
}
if (didUpdate) {
this.scheduleDiagnosticsUpdate(file);
}
}
public configFileDiagnosticsReceived(
file: vscode.Uri,
diagnostics: ReadonlyArray<vscode.Diagnostic>
): void {
this._currentDiagnostics.set(file, diagnostics);
}
public delete(resource: vscode.Uri): void {
this._currentDiagnostics.delete(resource);
this._diagnostics.delete(resource);
}
public getDiagnostics(file: vscode.Uri): ReadonlyArray<vscode.Diagnostic> {
return this._currentDiagnostics.get(file) || [];
}
private scheduleDiagnosticsUpdate(file: vscode.Uri) {
if (!this._pendingUpdates.has(file)) {
this._pendingUpdates.set(file, setTimeout(() => this.updateCurrentDiagnostics(file), this._updateDelay));
}
}
private updateCurrentDiagnostics(file: vscode.Uri): void {
if (this._pendingUpdates.has(file)) {
clearTimeout(this._pendingUpdates.get(file));
this._pendingUpdates.delete(file);
}
const fileDiagnostics = this._diagnostics.get(file);
this._currentDiagnostics.set(file, fileDiagnostics ? fileDiagnostics.getDiagnostics(this._settings) : []);
}
private rebuild(): void {
this._currentDiagnostics.clear();
for (const fileDiagnostic of this._diagnostics.values) {
this._currentDiagnostics.set(fileDiagnostic.file, fileDiagnostic.getDiagnostics(this._settings));
}
}
}

View File

@@ -0,0 +1,90 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { DocumentSelector } from '../utils/documentSelector';
const localize = nls.loadMessageBundle();
interface Directive {
readonly value: string;
readonly description: string;
}
const tsDirectives: Directive[] = [
{
value: '@ts-check',
description: localize(
'ts-check',
"Enables semantic checking in a JavaScript file. Must be at the top of a file.")
}, {
value: '@ts-nocheck',
description: localize(
'ts-nocheck',
"Disables semantic checking in a JavaScript file. Must be at the top of a file.")
}, {
value: '@ts-ignore',
description: localize(
'ts-ignore',
"Suppresses @ts-check errors on the next line of a file.")
}
];
const tsDirectives390: Directive[] = [
...tsDirectives,
{
value: '@ts-expect-error',
description: localize(
'ts-expect-error',
"Suppresses @ts-check errors on the next line of a file, expecting at least one to exist.")
}
];
class DirectiveCommentCompletionProvider implements vscode.CompletionItemProvider {
constructor(
private readonly client: ITypeScriptServiceClient,
) { }
public provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
_token: vscode.CancellationToken
): vscode.CompletionItem[] {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return [];
}
const line = document.lineAt(position.line).text;
const prefix = line.slice(0, position.character);
const match = prefix.match(/^\s*\/\/+\s?(@[a-zA-Z\-]*)?$/);
if (match) {
const directives = this.client.apiVersion.gte(API.v390)
? tsDirectives390
: tsDirectives;
return directives.map(directive => {
const item = new vscode.CompletionItem(directive.value, vscode.CompletionItemKind.Snippet);
item.detail = directive.description;
item.range = new vscode.Range(position.line, Math.max(0, position.character - (match[1] ? match[1].length : 0)), position.line, position.character);
return item;
});
}
return [];
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
) {
return vscode.languages.registerCompletionItemProvider(selector.syntax,
new DirectiveCommentCompletionProvider(client),
'@');
}

View File

@@ -0,0 +1,57 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { ITypeScriptServiceClient } from '../typescriptService';
import { flatten } from '../utils/arrays';
import { DocumentSelector } from '../utils/documentSelector';
import * as typeConverters from '../utils/typeConverters';
class TypeScriptDocumentHighlightProvider implements vscode.DocumentHighlightProvider {
public constructor(
private readonly client: ITypeScriptServiceClient
) { }
public async provideDocumentHighlights(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.DocumentHighlight[]> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return [];
}
const args = {
...typeConverters.Position.toFileLocationRequestArgs(file, position),
filesToSearch: [file]
};
const response = await this.client.execute('documentHighlights', args, token);
if (response.type !== 'response' || !response.body) {
return [];
}
return flatten(
response.body
.filter(highlight => highlight.file === file)
.map(convertDocumentHighlight));
}
}
function convertDocumentHighlight(highlight: Proto.DocumentHighlightsItem): ReadonlyArray<vscode.DocumentHighlight> {
return highlight.highlightSpans.map(span =>
new vscode.DocumentHighlight(
typeConverters.Range.fromTextSpan(span),
span.kind === 'writtenReference' ? vscode.DocumentHighlightKind.Write : vscode.DocumentHighlightKind.Read));
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
) {
return vscode.languages.registerDocumentHighlightProvider(selector.syntax,
new TypeScriptDocumentHighlightProvider(client));
}

View File

@@ -0,0 +1,134 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import * as PConst from '../protocol.const';
import { CachedResponse } from '../tsServer/cachedResponse';
import { ITypeScriptServiceClient } from '../typescriptService';
import { DocumentSelector } from '../utils/documentSelector';
import { parseKindModifier } from '../utils/modifiers';
import * as typeConverters from '../utils/typeConverters';
const getSymbolKind = (kind: string): vscode.SymbolKind => {
switch (kind) {
case PConst.Kind.module: return vscode.SymbolKind.Module;
case PConst.Kind.class: return vscode.SymbolKind.Class;
case PConst.Kind.enum: return vscode.SymbolKind.Enum;
case PConst.Kind.interface: return vscode.SymbolKind.Interface;
case PConst.Kind.method: return vscode.SymbolKind.Method;
case PConst.Kind.memberVariable: return vscode.SymbolKind.Property;
case PConst.Kind.memberGetAccessor: return vscode.SymbolKind.Property;
case PConst.Kind.memberSetAccessor: return vscode.SymbolKind.Property;
case PConst.Kind.variable: return vscode.SymbolKind.Variable;
case PConst.Kind.const: return vscode.SymbolKind.Variable;
case PConst.Kind.localVariable: return vscode.SymbolKind.Variable;
case PConst.Kind.function: return vscode.SymbolKind.Function;
case PConst.Kind.localFunction: return vscode.SymbolKind.Function;
case PConst.Kind.constructSignature: return vscode.SymbolKind.Constructor;
case PConst.Kind.constructorImplementation: return vscode.SymbolKind.Constructor;
}
return vscode.SymbolKind.Variable;
};
class TypeScriptDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
public constructor(
private readonly client: ITypeScriptServiceClient,
private cachedResponse: CachedResponse<Proto.NavTreeResponse>,
) { }
public async provideDocumentSymbols(document: vscode.TextDocument, token: vscode.CancellationToken): Promise<vscode.DocumentSymbol[] | undefined> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return undefined;
}
const args: Proto.FileRequestArgs = { file };
const response = await this.cachedResponse.execute(document, () => this.client.execute('navtree', args, token));
if (response.type !== 'response' || !response.body?.childItems) {
return undefined;
}
// The root represents the file. Ignore this when showing in the UI
const result: vscode.DocumentSymbol[] = [];
for (const item of response.body.childItems) {
TypeScriptDocumentSymbolProvider.convertNavTree(document.uri, result, item);
}
return result;
}
private static convertNavTree(
resource: vscode.Uri,
output: vscode.DocumentSymbol[],
item: Proto.NavigationTree,
): boolean {
let shouldInclude = TypeScriptDocumentSymbolProvider.shouldInclueEntry(item);
if (!shouldInclude && !item.childItems?.length) {
return false;
}
const children = new Set(item.childItems || []);
for (const span of item.spans) {
const range = typeConverters.Range.fromTextSpan(span);
const symbolInfo = TypeScriptDocumentSymbolProvider.convertSymbol(item, range);
for (const child of children) {
if (child.spans.some(span => !!range.intersection(typeConverters.Range.fromTextSpan(span)))) {
const includedChild = TypeScriptDocumentSymbolProvider.convertNavTree(resource, symbolInfo.children, child);
shouldInclude = shouldInclude || includedChild;
children.delete(child);
}
}
if (shouldInclude) {
output.push(symbolInfo);
}
}
return shouldInclude;
}
private static convertSymbol(item: Proto.NavigationTree, range: vscode.Range): vscode.DocumentSymbol {
const selectionRange = item.nameSpan ? typeConverters.Range.fromTextSpan(item.nameSpan) : range;
let label = item.text;
switch (item.kind) {
case PConst.Kind.memberGetAccessor: label = `(get) ${label}`; break;
case PConst.Kind.memberSetAccessor: label = `(set) ${label}`; break;
}
const symbolInfo = new vscode.DocumentSymbol(
label,
'',
getSymbolKind(item.kind),
range,
range.contains(selectionRange) ? selectionRange : range);
const kindModifiers = parseKindModifier(item.kindModifiers);
if (kindModifiers.has(PConst.KindModifiers.depreacted)) {
symbolInfo.tags = [vscode.SymbolTag.Deprecated];
}
return symbolInfo;
}
private static shouldInclueEntry(item: Proto.NavigationTree | Proto.NavigationBarItem): boolean {
if (item.kind === PConst.Kind.alias) {
return false;
}
return !!(item.text && item.text !== '<function>' && item.text !== '<class>');
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
cachedResponse: CachedResponse<Proto.NavTreeResponse>,
) {
return vscode.languages.registerDocumentSymbolProvider(selector.syntax,
new TypeScriptDocumentSymbolProvider(client, cachedResponse), { label: 'TypeScript' });
}

View File

@@ -0,0 +1,226 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { Disposable } from '../utils/dispose';
import * as fileSchemes from '../utils/fileSchemes';
import { isTypeScriptDocument } from '../utils/languageModeIds';
import { equals } from '../utils/objects';
import { ResourceMap } from '../utils/resourceMap';
namespace Experimental {
// https://github.com/microsoft/TypeScript/pull/37871/
export interface UserPreferences extends Proto.UserPreferences {
readonly provideRefactorNotApplicableReason?: boolean;
}
// https://github.com/microsoft/TypeScript/issues/41208
export interface FormatCodeSettings extends Proto.FormatCodeSettings {
readonly insertSpaceAfterOpeningAndBeforeClosingEmptyBraces?: boolean;
}
}
interface FileConfiguration {
readonly formatOptions: Proto.FormatCodeSettings;
readonly preferences: Proto.UserPreferences;
}
function areFileConfigurationsEqual(a: FileConfiguration, b: FileConfiguration): boolean {
return equals(a, b);
}
export default class FileConfigurationManager extends Disposable {
private readonly formatOptions: ResourceMap<Promise<FileConfiguration | undefined>>;
public constructor(
private readonly client: ITypeScriptServiceClient,
onCaseInsenitiveFileSystem: boolean
) {
super();
this.formatOptions = new ResourceMap(undefined, { onCaseInsenitiveFileSystem });
vscode.workspace.onDidCloseTextDocument(textDocument => {
// When a document gets closed delete the cached formatting options.
// This is necessary since the tsserver now closed a project when its
// last file in it closes which drops the stored formatting options
// as well.
this.formatOptions.delete(textDocument.uri);
}, undefined, this._disposables);
}
public async ensureConfigurationForDocument(
document: vscode.TextDocument,
token: vscode.CancellationToken
): Promise<void> {
const formattingOptions = this.getFormattingOptions(document);
if (formattingOptions) {
return this.ensureConfigurationOptions(document, formattingOptions, token);
}
}
private getFormattingOptions(
document: vscode.TextDocument
): vscode.FormattingOptions | undefined {
const editor = vscode.window.visibleTextEditors.find(editor => editor.document.fileName === document.fileName);
return editor
? {
tabSize: editor.options.tabSize,
insertSpaces: editor.options.insertSpaces
} as vscode.FormattingOptions
: undefined;
}
public async ensureConfigurationOptions(
document: vscode.TextDocument,
options: vscode.FormattingOptions,
token: vscode.CancellationToken
): Promise<void> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return;
}
const currentOptions = this.getFileOptions(document, options);
const cachedOptions = this.formatOptions.get(document.uri);
if (cachedOptions) {
const cachedOptionsValue = await cachedOptions;
if (cachedOptionsValue && areFileConfigurationsEqual(cachedOptionsValue, currentOptions)) {
return;
}
}
let resolve: (x: FileConfiguration | undefined) => void;
this.formatOptions.set(document.uri, new Promise<FileConfiguration | undefined>(r => resolve = r));
const args: Proto.ConfigureRequestArguments = {
file,
...currentOptions,
};
try {
const response = await this.client.execute('configure', args, token);
resolve!(response.type === 'response' ? currentOptions : undefined);
} finally {
resolve!(undefined);
}
}
public async setGlobalConfigurationFromDocument(
document: vscode.TextDocument,
token: vscode.CancellationToken,
): Promise<void> {
const formattingOptions = this.getFormattingOptions(document);
if (!formattingOptions) {
return;
}
const args: Proto.ConfigureRequestArguments = {
file: undefined /*global*/,
...this.getFileOptions(document, formattingOptions),
};
await this.client.execute('configure', args, token);
}
public reset() {
this.formatOptions.clear();
}
private getFileOptions(
document: vscode.TextDocument,
options: vscode.FormattingOptions
): FileConfiguration {
return {
formatOptions: this.getFormatOptions(document, options),
preferences: this.getPreferences(document)
};
}
private getFormatOptions(
document: vscode.TextDocument,
options: vscode.FormattingOptions
): Experimental.FormatCodeSettings {
const config = vscode.workspace.getConfiguration(
isTypeScriptDocument(document) ? 'typescript.format' : 'javascript.format',
document.uri);
return {
tabSize: options.tabSize,
indentSize: options.tabSize,
convertTabsToSpaces: options.insertSpaces,
// We can use \n here since the editor normalizes later on to its line endings.
newLineCharacter: '\n',
insertSpaceAfterCommaDelimiter: config.get<boolean>('insertSpaceAfterCommaDelimiter'),
insertSpaceAfterConstructor: config.get<boolean>('insertSpaceAfterConstructor'),
insertSpaceAfterSemicolonInForStatements: config.get<boolean>('insertSpaceAfterSemicolonInForStatements'),
insertSpaceBeforeAndAfterBinaryOperators: config.get<boolean>('insertSpaceBeforeAndAfterBinaryOperators'),
insertSpaceAfterKeywordsInControlFlowStatements: config.get<boolean>('insertSpaceAfterKeywordsInControlFlowStatements'),
insertSpaceAfterFunctionKeywordForAnonymousFunctions: config.get<boolean>('insertSpaceAfterFunctionKeywordForAnonymousFunctions'),
insertSpaceBeforeFunctionParenthesis: config.get<boolean>('insertSpaceBeforeFunctionParenthesis'),
insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: config.get<boolean>('insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis'),
insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: config.get<boolean>('insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets'),
insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: config.get<boolean>('insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces'),
insertSpaceAfterOpeningAndBeforeClosingEmptyBraces: config.get<boolean>('insertSpaceAfterOpeningAndBeforeClosingEmptyBraces'),
insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: config.get<boolean>('insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces'),
insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: config.get<boolean>('insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces'),
insertSpaceAfterTypeAssertion: config.get<boolean>('insertSpaceAfterTypeAssertion'),
placeOpenBraceOnNewLineForFunctions: config.get<boolean>('placeOpenBraceOnNewLineForFunctions'),
placeOpenBraceOnNewLineForControlBlocks: config.get<boolean>('placeOpenBraceOnNewLineForControlBlocks'),
semicolons: config.get<Proto.SemicolonPreference>('semicolons'),
};
}
private getPreferences(document: vscode.TextDocument): Proto.UserPreferences {
if (this.client.apiVersion.lt(API.v290)) {
return {};
}
const config = vscode.workspace.getConfiguration(
isTypeScriptDocument(document) ? 'typescript' : 'javascript',
document.uri);
const preferencesConfig = vscode.workspace.getConfiguration(
isTypeScriptDocument(document) ? 'typescript.preferences' : 'javascript.preferences',
document.uri);
const preferences: Experimental.UserPreferences = {
quotePreference: this.getQuoteStylePreference(preferencesConfig),
importModuleSpecifierPreference: getImportModuleSpecifierPreference(preferencesConfig),
importModuleSpecifierEnding: getImportModuleSpecifierEndingPreference(preferencesConfig),
allowTextChangesInNewFiles: document.uri.scheme === fileSchemes.file,
providePrefixAndSuffixTextForRename: preferencesConfig.get<boolean>('renameShorthandProperties', true) === false ? false : preferencesConfig.get<boolean>('useAliasesForRenames', true),
allowRenameOfImportPath: true,
includeAutomaticOptionalChainCompletions: config.get<boolean>('suggest.includeAutomaticOptionalChainCompletions', true),
provideRefactorNotApplicableReason: true,
};
return preferences;
}
private getQuoteStylePreference(config: vscode.WorkspaceConfiguration) {
switch (config.get<string>('quoteStyle')) {
case 'single': return 'single';
case 'double': return 'double';
default: return this.client.apiVersion.gte(API.v333) ? 'auto' : undefined;
}
}
}
function getImportModuleSpecifierPreference(config: vscode.WorkspaceConfiguration) {
switch (config.get<string>('importModuleSpecifier')) {
case 'relative': return 'relative';
case 'non-relative': return 'non-relative';
default: return undefined;
}
}
function getImportModuleSpecifierEndingPreference(config: vscode.WorkspaceConfiguration) {
switch (config.get<string>('importModuleSpecifierEnding')) {
case 'minimal': return 'minimal';
case 'index': return 'index';
case 'js': return 'js';
default: return 'auto';
}
}

View File

@@ -0,0 +1,265 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import type * as Proto from '../protocol';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { conditionalRegistration, requireSomeCapability, requireMinVersion } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as errorCodes from '../utils/errorCodes';
import * as fixNames from '../utils/fixNames';
import * as typeConverters from '../utils/typeConverters';
import { DiagnosticsManager } from './diagnostics';
import FileConfigurationManager from './fileConfigurationManager';
const localize = nls.loadMessageBundle();
interface AutoFix {
readonly codes: Set<number>;
readonly fixName: string;
}
async function buildIndividualFixes(
fixes: readonly AutoFix[],
edit: vscode.WorkspaceEdit,
client: ITypeScriptServiceClient,
file: string,
diagnostics: readonly vscode.Diagnostic[],
token: vscode.CancellationToken,
): Promise<void> {
for (const diagnostic of diagnostics) {
for (const { codes, fixName } of fixes) {
if (token.isCancellationRequested) {
return;
}
if (!codes.has(diagnostic.code as number)) {
continue;
}
const args: Proto.CodeFixRequestArgs = {
...typeConverters.Range.toFileRangeRequestArgs(file, diagnostic.range),
errorCodes: [+(diagnostic.code!)]
};
const response = await client.execute('getCodeFixes', args, token);
if (response.type !== 'response') {
continue;
}
const fix = response.body?.find(fix => fix.fixName === fixName);
if (fix) {
typeConverters.WorkspaceEdit.withFileCodeEdits(edit, client, fix.changes);
break;
}
}
}
}
async function buildCombinedFix(
fixes: readonly AutoFix[],
edit: vscode.WorkspaceEdit,
client: ITypeScriptServiceClient,
file: string,
diagnostics: readonly vscode.Diagnostic[],
token: vscode.CancellationToken,
): Promise<void> {
for (const diagnostic of diagnostics) {
for (const { codes, fixName } of fixes) {
if (token.isCancellationRequested) {
return;
}
if (!codes.has(diagnostic.code as number)) {
continue;
}
const args: Proto.CodeFixRequestArgs = {
...typeConverters.Range.toFileRangeRequestArgs(file, diagnostic.range),
errorCodes: [+(diagnostic.code!)]
};
const response = await client.execute('getCodeFixes', args, token);
if (response.type !== 'response' || !response.body?.length) {
continue;
}
const fix = response.body?.find(fix => fix.fixName === fixName);
if (!fix) {
continue;
}
if (!fix.fixId) {
typeConverters.WorkspaceEdit.withFileCodeEdits(edit, client, fix.changes);
return;
}
const combinedArgs: Proto.GetCombinedCodeFixRequestArgs = {
scope: {
type: 'file',
args: { file }
},
fixId: fix.fixId,
};
const combinedResponse = await client.execute('getCombinedCodeFix', combinedArgs, token);
if (combinedResponse.type !== 'response' || !combinedResponse.body) {
return;
}
typeConverters.WorkspaceEdit.withFileCodeEdits(edit, client, combinedResponse.body.changes);
return;
}
}
}
// #region Source Actions
abstract class SourceAction extends vscode.CodeAction {
abstract build(
client: ITypeScriptServiceClient,
file: string,
diagnostics: readonly vscode.Diagnostic[],
token: vscode.CancellationToken,
): Promise<void>;
}
class SourceFixAll extends SourceAction {
static readonly kind = vscode.CodeActionKind.SourceFixAll.append('ts');
constructor() {
super(localize('autoFix.label', 'Fix All'), SourceFixAll.kind);
}
async build(client: ITypeScriptServiceClient, file: string, diagnostics: readonly vscode.Diagnostic[], token: vscode.CancellationToken): Promise<void> {
this.edit = new vscode.WorkspaceEdit();
await buildIndividualFixes([
{ codes: errorCodes.incorrectlyImplementsInterface, fixName: fixNames.classIncorrectlyImplementsInterface },
{ codes: errorCodes.asyncOnlyAllowedInAsyncFunctions, fixName: fixNames.awaitInSyncFunction },
], this.edit, client, file, diagnostics, token);
await buildCombinedFix([
{ codes: errorCodes.unreachableCode, fixName: fixNames.unreachableCode }
], this.edit, client, file, diagnostics, token);
}
}
class SourceRemoveUnused extends SourceAction {
static readonly kind = vscode.CodeActionKind.Source.append('removeUnused').append('ts');
constructor() {
super(localize('autoFix.unused.label', 'Remove all unused code'), SourceRemoveUnused.kind);
}
async build(client: ITypeScriptServiceClient, file: string, diagnostics: readonly vscode.Diagnostic[], token: vscode.CancellationToken): Promise<void> {
this.edit = new vscode.WorkspaceEdit();
await buildCombinedFix([
{ codes: errorCodes.variableDeclaredButNeverUsed, fixName: fixNames.unusedIdentifier },
], this.edit, client, file, diagnostics, token);
}
}
class SourceAddMissingImports extends SourceAction {
static readonly kind = vscode.CodeActionKind.Source.append('addMissingImports').append('ts');
constructor() {
super(localize('autoFix.missingImports.label', 'Add all missing imports'), SourceAddMissingImports.kind);
}
async build(client: ITypeScriptServiceClient, file: string, diagnostics: readonly vscode.Diagnostic[], token: vscode.CancellationToken): Promise<void> {
this.edit = new vscode.WorkspaceEdit();
await buildCombinedFix([
{ codes: errorCodes.cannotFindName, fixName: fixNames.fixImport }
],
this.edit, client, file, diagnostics, token);
}
}
//#endregion
class TypeScriptAutoFixProvider implements vscode.CodeActionProvider {
private static kindProviders = [
SourceFixAll,
SourceRemoveUnused,
SourceAddMissingImports,
];
constructor(
private readonly client: ITypeScriptServiceClient,
private readonly fileConfigurationManager: FileConfigurationManager,
private readonly diagnosticsManager: DiagnosticsManager,
) { }
public get metadata(): vscode.CodeActionProviderMetadata {
return {
providedCodeActionKinds: TypeScriptAutoFixProvider.kindProviders.map(x => x.kind),
};
}
public async provideCodeActions(
document: vscode.TextDocument,
_range: vscode.Range,
context: vscode.CodeActionContext,
token: vscode.CancellationToken
): Promise<vscode.CodeAction[] | undefined> {
if (!context.only || !vscode.CodeActionKind.Source.intersects(context.only)) {
return undefined;
}
const file = this.client.toOpenedFilePath(document);
if (!file) {
return undefined;
}
const actions = this.getFixAllActions(context.only);
if (this.client.bufferSyncSupport.hasPendingDiagnostics(document.uri)) {
return actions;
}
const diagnostics = this.diagnosticsManager.getDiagnostics(document.uri);
if (!diagnostics.length) {
// Actions are a no-op in this case but we still want to return them
return actions;
}
await this.fileConfigurationManager.ensureConfigurationForDocument(document, token);
if (token.isCancellationRequested) {
return undefined;
}
await Promise.all(actions.map(action => action.build(this.client, file, diagnostics, token)));
return actions;
}
private getFixAllActions(only: vscode.CodeActionKind): SourceAction[] {
return TypeScriptAutoFixProvider.kindProviders
.filter(provider => only.intersects(provider.kind))
.map(provider => new provider());
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
fileConfigurationManager: FileConfigurationManager,
diagnosticsManager: DiagnosticsManager,
) {
return conditionalRegistration([
requireMinVersion(client, API.v300),
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
const provider = new TypeScriptAutoFixProvider(client, fileConfigurationManager, diagnosticsManager);
return vscode.languages.registerCodeActionsProvider(selector.semantic, provider, provider.metadata);
});
}

View File

@@ -0,0 +1,86 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { coalesce } from '../utils/arrays';
import { conditionalRegistration, requireMinVersion } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as typeConverters from '../utils/typeConverters';
class TypeScriptFoldingProvider implements vscode.FoldingRangeProvider {
public static readonly minVersion = API.v280;
public constructor(
private readonly client: ITypeScriptServiceClient
) { }
async provideFoldingRanges(
document: vscode.TextDocument,
_context: vscode.FoldingContext,
token: vscode.CancellationToken
): Promise<vscode.FoldingRange[] | undefined> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return;
}
const args: Proto.FileRequestArgs = { file };
const response = await this.client.execute('getOutliningSpans', args, token);
if (response.type !== 'response' || !response.body) {
return;
}
return coalesce(response.body.map(span => this.convertOutliningSpan(span, document)));
}
private convertOutliningSpan(
span: Proto.OutliningSpan,
document: vscode.TextDocument
): vscode.FoldingRange | undefined {
const range = typeConverters.Range.fromTextSpan(span.textSpan);
const kind = TypeScriptFoldingProvider.getFoldingRangeKind(span);
// Workaround for #49904
if (span.kind === 'comment') {
const line = document.lineAt(range.start.line).text;
if (line.match(/\/\/\s*#endregion/gi)) {
return undefined;
}
}
const start = range.start.line;
// workaround for #47240
const end = (range.end.character > 0 && ['}', ']'].includes(document.getText(new vscode.Range(range.end.translate(0, -1), range.end))))
? Math.max(range.end.line - 1, range.start.line)
: range.end.line;
return new vscode.FoldingRange(start, end, kind);
}
private static getFoldingRangeKind(span: Proto.OutliningSpan): vscode.FoldingRangeKind | undefined {
switch (span.kind) {
case 'comment': return vscode.FoldingRangeKind.Comment;
case 'region': return vscode.FoldingRangeKind.Region;
case 'imports': return vscode.FoldingRangeKind.Imports;
case 'code':
default: return undefined;
}
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
): vscode.Disposable {
return conditionalRegistration([
requireMinVersion(client, TypeScriptFoldingProvider.minVersion),
], () => {
return vscode.languages.registerFoldingRangeProvider(selector.syntax,
new TypeScriptFoldingProvider(client));
});
}

View File

@@ -0,0 +1,102 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { ITypeScriptServiceClient } from '../typescriptService';
import { conditionalRegistration, requireConfiguration } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as typeConverters from '../utils/typeConverters';
import FileConfigurationManager from './fileConfigurationManager';
class TypeScriptFormattingProvider implements vscode.DocumentRangeFormattingEditProvider, vscode.OnTypeFormattingEditProvider {
public constructor(
private readonly client: ITypeScriptServiceClient,
private readonly formattingOptionsManager: FileConfigurationManager
) { }
public async provideDocumentRangeFormattingEdits(
document: vscode.TextDocument,
range: vscode.Range,
options: vscode.FormattingOptions,
token: vscode.CancellationToken
): Promise<vscode.TextEdit[] | undefined> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return undefined;
}
await this.formattingOptionsManager.ensureConfigurationOptions(document, options, token);
const args = typeConverters.Range.toFormattingRequestArgs(file, range);
const response = await this.client.execute('format', args, token);
if (response.type !== 'response' || !response.body) {
return undefined;
}
return response.body.map(typeConverters.TextEdit.fromCodeEdit);
}
public async provideOnTypeFormattingEdits(
document: vscode.TextDocument,
position: vscode.Position,
ch: string,
options: vscode.FormattingOptions,
token: vscode.CancellationToken
): Promise<vscode.TextEdit[]> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return [];
}
await this.formattingOptionsManager.ensureConfigurationOptions(document, options, token);
const args: Proto.FormatOnKeyRequestArgs = {
...typeConverters.Position.toFileLocationRequestArgs(file, position),
key: ch
};
const response = await this.client.execute('formatonkey', args, token);
if (response.type !== 'response' || !response.body) {
return [];
}
const result: vscode.TextEdit[] = [];
for (const edit of response.body) {
const textEdit = typeConverters.TextEdit.fromCodeEdit(edit);
const range = textEdit.range;
// Work around for https://github.com/microsoft/TypeScript/issues/6700.
// Check if we have an edit at the beginning of the line which only removes white spaces and leaves
// an empty line. Drop those edits
if (range.start.character === 0 && range.start.line === range.end.line && textEdit.newText === '') {
const lText = document.lineAt(range.start.line).text;
// If the edit leaves something on the line keep the edit (note that the end character is exclusive).
// Keep it also if it removes something else than whitespace
if (lText.trim().length > 0 || lText.length > range.end.character) {
result.push(textEdit);
}
} else {
result.push(textEdit);
}
}
return result;
}
}
export function register(
selector: DocumentSelector,
modeId: string,
client: ITypeScriptServiceClient,
fileConfigurationManager: FileConfigurationManager
) {
return conditionalRegistration([
requireConfiguration(modeId, 'format.enable'),
], () => {
const formattingProvider = new TypeScriptFormattingProvider(client, fileConfigurationManager);
return vscode.Disposable.from(
vscode.languages.registerOnTypeFormattingEditProvider(selector.syntax, formattingProvider, ';', '}', '\n'),
vscode.languages.registerDocumentRangeFormattingEditProvider(selector.syntax, formattingProvider),
);
});
}

View File

@@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { localize } from '../tsServer/versionProvider';
import { ClientCapability, ITypeScriptServiceClient, ServerType } from '../typescriptService';
import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import { markdownDocumentation } from '../utils/previewer';
import * as typeConverters from '../utils/typeConverters';
class TypeScriptHoverProvider implements vscode.HoverProvider {
public constructor(
private readonly client: ITypeScriptServiceClient
) { }
public async provideHover(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.Hover | undefined> {
const filepath = this.client.toOpenedFilePath(document);
if (!filepath) {
return undefined;
}
const args = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
const response = await this.client.interruptGetErr(() => this.client.execute('quickinfo', args, token));
if (response.type !== 'response' || !response.body) {
return undefined;
}
return new vscode.Hover(
this.getContents(document.uri, response.body, response._serverType),
typeConverters.Range.fromTextSpan(response.body));
}
private getContents(
resource: vscode.Uri,
data: Proto.QuickInfoResponseBody,
source: ServerType | undefined,
) {
const parts: vscode.MarkedString[] = [];
if (data.displayString) {
const displayParts: string[] = [];
if (source === ServerType.Syntax && this.client.hasCapabilityForResource(resource, ClientCapability.Semantic)) {
displayParts.push(
localize({
key: 'loadingPrefix',
comment: ['Prefix displayed for hover entries while the server is still loading']
}, "(loading...)"));
}
displayParts.push(data.displayString);
parts.push({ language: 'typescript', value: displayParts.join(' ') });
}
parts.push(markdownDocumentation(data.documentation, data.tags));
return parts;
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient
): vscode.Disposable {
return conditionalRegistration([
requireSomeCapability(client, ClientCapability.EnhancedSyntax, ClientCapability.Semantic),
], () => {
return vscode.languages.registerHoverProvider(selector.syntax,
new TypeScriptHoverProvider(client));
});
}

View File

@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import DefinitionProviderBase from './definitionProviderBase';
class TypeScriptImplementationProvider extends DefinitionProviderBase implements vscode.ImplementationProvider {
public provideImplementation(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Definition | undefined> {
return this.getSymbolLocations('implementation', document, position, token);
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
) {
return conditionalRegistration([
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
return vscode.languages.registerImplementationProvider(selector.semantic,
new TypeScriptImplementationProvider(client));
});
}

View File

@@ -0,0 +1,125 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { ITypeScriptServiceClient } from '../typescriptService';
import { conditionalRegistration, requireConfiguration } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as typeConverters from '../utils/typeConverters';
const localize = nls.loadMessageBundle();
const defaultJsDoc = new vscode.SnippetString(`/**\n * $0\n */`);
class JsDocCompletionItem extends vscode.CompletionItem {
constructor(
public readonly document: vscode.TextDocument,
public readonly position: vscode.Position
) {
super('/** */', vscode.CompletionItemKind.Snippet);
this.detail = localize('typescript.jsDocCompletionItem.documentation', 'JSDoc comment');
this.sortText = '\0';
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.translate(0, prefix ? -prefix[0].length : 0);
const range = new vscode.Range(start, position.translate(0, suffix ? suffix[0].length : 0));
this.range = { inserting: range, replacing: range };
}
}
class JsDocCompletionProvider implements vscode.CompletionItemProvider {
constructor(
private readonly client: ITypeScriptServiceClient,
) { }
public async provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.CompletionItem[] | undefined> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return undefined;
}
if (!this.isPotentiallyValidDocCompletionPosition(document, position)) {
return undefined;
}
const args = typeConverters.Position.toFileLocationRequestArgs(file, position);
const response = await this.client.execute('docCommentTemplate', args, token);
if (response.type !== 'response' || !response.body) {
return undefined;
}
const item = new JsDocCompletionItem(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.insertText = defaultJsDoc;
} else {
item.insertText = templateToSnippet(response.body.newText);
}
return [item];
}
private isPotentiallyValidDocCompletionPosition(
document: vscode.TextDocument,
position: vscode.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): vscode.SnippetString {
// TODO: use append placeholder
let snippetIndex = 1;
template = template.replace(/\$/g, '\\$');
template = template.replace(/^\s*(?=(\/|[ ]\*))/gm, '');
template = template.replace(/^(\/\*\*\s*\*[ ]*)$/m, (x) => x + `\$0`);
template = template.replace(/\* @param([ ]\{\S+\})?\s+(\S+)\s*$/gm, (_param, type, post) => {
let out = '* @param ';
if (type === ' {any}' || type === ' {*}') {
out += `{\$\{${snippetIndex++}:*\}} `;
} else if (type) {
out += type + ' ';
}
out += post + ` \${${snippetIndex++}}`;
return out;
});
return new vscode.SnippetString(template);
}
export function register(
selector: DocumentSelector,
modeId: string,
client: ITypeScriptServiceClient,
): vscode.Disposable {
return conditionalRegistration([
requireConfiguration(modeId, 'suggest.completeJSDocs')
], () => {
return vscode.languages.registerCompletionItemProvider(selector.syntax,
new JsDocCompletionProvider(client),
'*');
});
}

View File

@@ -0,0 +1,103 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* --------------------------------------------------------------------------------------------
* Includes code from typescript-sublime-plugin project, obtained from
* https://github.com/microsoft/TypeScript-Sublime-Plugin/blob/master/TypeScript%20Indent.tmPreferences
* ------------------------------------------------------------------------------------------ */
import * as vscode from 'vscode';
import { Disposable } from '../utils/dispose';
import * as languageModeIds from '../utils/languageModeIds';
const jsTsLanguageConfiguration: vscode.LanguageConfiguration = {
indentationRules: {
decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]].*$/,
increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/
},
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
onEnterRules: [
{
// e.g. /** | */
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
afterText: /^\s*\*\/$/,
action: { indentAction: vscode.IndentAction.IndentOutdent, appendText: ' * ' },
}, {
// e.g. /** ...|
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
action: { indentAction: vscode.IndentAction.None, appendText: ' * ' },
}, {
// e.g. * ...|
beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/,
oneLineAboveText: /(?=^(\s*(\/\*\*|\*)).*)(?=(?!(\s*\*\/)))/,
action: { indentAction: vscode.IndentAction.None, appendText: '* ' },
}, {
// e.g. */|
beforeText: /^(\t|[ ])*[ ]\*\/\s*$/,
action: { indentAction: vscode.IndentAction.None, removeText: 1 },
},
{
// e.g. *-----*/|
beforeText: /^(\t|[ ])*[ ]\*[^/]*\*\/\s*$/,
action: { indentAction: vscode.IndentAction.None, removeText: 1 },
},
{
beforeText: /^\s*(\bcase\s.+:|\bdefault:)$/,
afterText: /^(?!\s*(\bcase\b|\bdefault\b))/,
action: { indentAction: vscode.IndentAction.Indent },
}
]
};
const EMPTY_ELEMENTS: string[] = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'];
const jsxTagsLanguageConfiguration: vscode.LanguageConfiguration = {
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,
onEnterRules: [
{
beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))([_:\\w][_:\\w\\-.\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>$/i,
action: { indentAction: vscode.IndentAction.IndentOutdent }
},
{
beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))([_:\\w][_:\\w\\-.\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
action: { indentAction: vscode.IndentAction.Indent }
},
{
// `beforeText` only applies to tokens of a given language. Since we are dealing with jsx-tags,
// make sure we apply to the closing `>` of a tag so that mixed language spans
// such as `<div onclick={1}>` are handled properly.
beforeText: /^>$/,
afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>$/i,
action: { indentAction: vscode.IndentAction.IndentOutdent }
},
{
beforeText: /^>$/,
action: { indentAction: vscode.IndentAction.Indent }
},
],
};
export class LanguageConfigurationManager extends Disposable {
constructor() {
super();
const standardLanguages = [
languageModeIds.javascript,
languageModeIds.javascriptreact,
languageModeIds.typescript,
languageModeIds.typescriptreact,
];
for (const language of standardLanguages) {
this.registerConfiguration(language, jsTsLanguageConfiguration);
}
this.registerConfiguration(languageModeIds.jsxTags, jsxTagsLanguageConfiguration);
}
private registerConfiguration(language: string, config: vscode.LanguageConfiguration) {
this._register(vscode.languages.setLanguageConfiguration(language, config));
}
}

View File

@@ -0,0 +1,118 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import type * as Proto from '../protocol';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { nulToken } from '../utils/cancellation';
import { Command, CommandManager } from '../commands/commandManager';
import { conditionalRegistration, requireMinVersion, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import { TelemetryReporter } from '../utils/telemetry';
import * as typeconverts from '../utils/typeConverters';
import FileConfigurationManager from './fileConfigurationManager';
const localize = nls.loadMessageBundle();
class OrganizeImportsCommand implements Command {
public static readonly Id = '_typescript.organizeImports';
public readonly id = OrganizeImportsCommand.Id;
constructor(
private readonly client: ITypeScriptServiceClient,
private readonly telemetryReporter: TelemetryReporter,
) { }
public async execute(file: string): Promise<boolean> {
/* __GDPR__
"organizeImports.execute" : {
"${include}": [
"${TypeScriptCommonProperties}"
]
}
*/
this.telemetryReporter.logTelemetry('organizeImports.execute', {});
const args: Proto.OrganizeImportsRequestArgs = {
scope: {
type: 'file',
args: {
file
}
}
};
const response = await this.client.interruptGetErr(() => this.client.execute('organizeImports', args, nulToken));
if (response.type !== 'response' || !response.body) {
return false;
}
const edits = typeconverts.WorkspaceEdit.fromFileCodeEdits(this.client, response.body);
return vscode.workspace.applyEdit(edits);
}
}
export class OrganizeImportsCodeActionProvider implements vscode.CodeActionProvider {
public static readonly minVersion = API.v280;
public constructor(
private readonly client: ITypeScriptServiceClient,
commandManager: CommandManager,
private readonly fileConfigManager: FileConfigurationManager,
telemetryReporter: TelemetryReporter,
) {
commandManager.register(new OrganizeImportsCommand(client, telemetryReporter));
}
public readonly metadata: vscode.CodeActionProviderMetadata = {
providedCodeActionKinds: [vscode.CodeActionKind.SourceOrganizeImports]
};
public provideCodeActions(
document: vscode.TextDocument,
_range: vscode.Range,
context: vscode.CodeActionContext,
token: vscode.CancellationToken
): vscode.CodeAction[] {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return [];
}
if (!context.only || !context.only.contains(vscode.CodeActionKind.SourceOrganizeImports)) {
return [];
}
this.fileConfigManager.ensureConfigurationForDocument(document, token);
const action = new vscode.CodeAction(
localize('organizeImportsAction.title', "Organize Imports"),
vscode.CodeActionKind.SourceOrganizeImports);
action.command = { title: '', command: OrganizeImportsCommand.Id, arguments: [file] };
return [action];
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
commandManager: CommandManager,
fileConfigurationManager: FileConfigurationManager,
telemetryReporter: TelemetryReporter,
) {
return conditionalRegistration([
requireMinVersion(client, OrganizeImportsCodeActionProvider.minVersion),
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
const organizeImportsProvider = new OrganizeImportsCodeActionProvider(client, commandManager, fileConfigurationManager, telemetryReporter);
return vscode.languages.registerCodeActionsProvider(selector.semantic,
organizeImportsProvider,
organizeImportsProvider.metadata);
});
}

View File

@@ -0,0 +1,421 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { Command, CommandManager } from '../commands/commandManager';
import type * as Proto from '../protocol';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { nulToken } from '../utils/cancellation';
import { applyCodeActionCommands, getEditForCodeAction } from '../utils/codeAction';
import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as fixNames from '../utils/fixNames';
import { memoize } from '../utils/memoize';
import { equals } from '../utils/objects';
import { TelemetryReporter } from '../utils/telemetry';
import * as typeConverters from '../utils/typeConverters';
import { DiagnosticsManager } from './diagnostics';
import FileConfigurationManager from './fileConfigurationManager';
const localize = nls.loadMessageBundle();
class ApplyCodeActionCommand implements Command {
public static readonly ID = '_typescript.applyCodeActionCommand';
public readonly id = ApplyCodeActionCommand.ID;
constructor(
private readonly client: ITypeScriptServiceClient,
private readonly telemetryReporter: TelemetryReporter,
) { }
public async execute(
action: Proto.CodeFixAction
): Promise<boolean> {
/* __GDPR__
"quickFix.execute" : {
"fixName" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" },
"${include}": [
"${TypeScriptCommonProperties}"
]
}
*/
this.telemetryReporter.logTelemetry('quickFix.execute', {
fixName: action.fixName
});
return applyCodeActionCommands(this.client, action.commands, nulToken);
}
}
class ApplyFixAllCodeAction implements Command {
public static readonly ID = '_typescript.applyFixAllCodeAction';
public readonly id = ApplyFixAllCodeAction.ID;
constructor(
private readonly client: ITypeScriptServiceClient,
private readonly telemetryReporter: TelemetryReporter,
) { }
public async execute(
file: string,
tsAction: Proto.CodeFixAction,
): Promise<void> {
if (!tsAction.fixId) {
return;
}
/* __GDPR__
"quickFixAll.execute" : {
"fixName" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" },
"${include}": [
"${TypeScriptCommonProperties}"
]
}
*/
this.telemetryReporter.logTelemetry('quickFixAll.execute', {
fixName: tsAction.fixName
});
const args: Proto.GetCombinedCodeFixRequestArgs = {
scope: {
type: 'file',
args: { file }
},
fixId: tsAction.fixId,
};
const response = await this.client.execute('getCombinedCodeFix', args, nulToken);
if (response.type !== 'response' || !response.body) {
return undefined;
}
const edit = typeConverters.WorkspaceEdit.fromFileCodeEdits(this.client, response.body.changes);
await vscode.workspace.applyEdit(edit);
await applyCodeActionCommands(this.client, response.body.commands, nulToken);
}
}
/**
* Unique set of diagnostics keyed on diagnostic range and error code.
*/
class DiagnosticsSet {
public static from(diagnostics: vscode.Diagnostic[]) {
const values = new Map<string, vscode.Diagnostic>();
for (const diagnostic of diagnostics) {
values.set(DiagnosticsSet.key(diagnostic), diagnostic);
}
return new DiagnosticsSet(values);
}
private static key(diagnostic: vscode.Diagnostic) {
const { start, end } = diagnostic.range;
return `${diagnostic.code}-${start.line},${start.character}-${end.line},${end.character}`;
}
private constructor(
private readonly _values: Map<string, vscode.Diagnostic>
) { }
public get values(): Iterable<vscode.Diagnostic> {
return this._values.values();
}
public get size() {
return this._values.size;
}
}
class VsCodeCodeAction extends vscode.CodeAction {
constructor(
public readonly tsAction: Proto.CodeFixAction,
title: string,
kind: vscode.CodeActionKind,
public readonly isFixAll: boolean,
) {
super(title, kind);
}
}
class CodeActionSet {
private readonly _actions = new Set<VsCodeCodeAction>();
private readonly _fixAllActions = new Map<{}, VsCodeCodeAction>();
public get values(): Iterable<VsCodeCodeAction> {
return this._actions;
}
public addAction(action: VsCodeCodeAction) {
for (const existing of this._actions) {
if (action.tsAction.fixName === existing.tsAction.fixName && equals(action.edit, existing.edit)) {
this._actions.delete(existing);
}
}
this._actions.add(action);
if (action.tsAction.fixId) {
// If we have an existing fix all action, then make sure it follows this action
const existingFixAll = this._fixAllActions.get(action.tsAction.fixId);
if (existingFixAll) {
this._actions.delete(existingFixAll);
this._actions.add(existingFixAll);
}
}
}
public addFixAllAction(fixId: {}, action: VsCodeCodeAction) {
const existing = this._fixAllActions.get(fixId);
if (existing) {
// reinsert action at back of actions list
this._actions.delete(existing);
}
this.addAction(action);
this._fixAllActions.set(fixId, action);
}
public hasFixAllAction(fixId: {}) {
return this._fixAllActions.has(fixId);
}
}
class SupportedCodeActionProvider {
public constructor(
private readonly client: ITypeScriptServiceClient
) { }
public async getFixableDiagnosticsForContext(context: vscode.CodeActionContext): Promise<DiagnosticsSet> {
const fixableCodes = await this.fixableDiagnosticCodes;
return DiagnosticsSet.from(
context.diagnostics.filter(diagnostic => typeof diagnostic.code !== 'undefined' && fixableCodes.has(diagnostic.code + '')));
}
@memoize
private get fixableDiagnosticCodes(): Thenable<Set<string>> {
return this.client.execute('getSupportedCodeFixes', null, nulToken)
.then(response => response.type === 'response' ? response.body || [] : [])
.then(codes => new Set(codes));
}
}
class TypeScriptQuickFixProvider implements vscode.CodeActionProvider {
public static readonly metadata: vscode.CodeActionProviderMetadata = {
providedCodeActionKinds: [vscode.CodeActionKind.QuickFix]
};
private readonly supportedCodeActionProvider: SupportedCodeActionProvider;
constructor(
private readonly client: ITypeScriptServiceClient,
private readonly formattingConfigurationManager: FileConfigurationManager,
commandManager: CommandManager,
private readonly diagnosticsManager: DiagnosticsManager,
telemetryReporter: TelemetryReporter
) {
commandManager.register(new ApplyCodeActionCommand(client, telemetryReporter));
commandManager.register(new ApplyFixAllCodeAction(client, telemetryReporter));
this.supportedCodeActionProvider = new SupportedCodeActionProvider(client);
}
public async provideCodeActions(
document: vscode.TextDocument,
_range: vscode.Range,
context: vscode.CodeActionContext,
token: vscode.CancellationToken
): Promise<vscode.CodeAction[]> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return [];
}
const fixableDiagnostics = await this.supportedCodeActionProvider.getFixableDiagnosticsForContext(context);
if (!fixableDiagnostics.size) {
return [];
}
if (this.client.bufferSyncSupport.hasPendingDiagnostics(document.uri)) {
return [];
}
await this.formattingConfigurationManager.ensureConfigurationForDocument(document, token);
const results = new CodeActionSet();
for (const diagnostic of fixableDiagnostics.values) {
await this.getFixesForDiagnostic(document, file, diagnostic, results, token);
}
const allActions = Array.from(results.values);
for (const action of allActions) {
action.isPreferred = isPreferredFix(action, allActions);
}
return allActions;
}
private async getFixesForDiagnostic(
document: vscode.TextDocument,
file: string,
diagnostic: vscode.Diagnostic,
results: CodeActionSet,
token: vscode.CancellationToken,
): Promise<CodeActionSet> {
const args: Proto.CodeFixRequestArgs = {
...typeConverters.Range.toFileRangeRequestArgs(file, diagnostic.range),
errorCodes: [+(diagnostic.code!)]
};
const response = await this.client.execute('getCodeFixes', args, token);
if (response.type !== 'response' || !response.body) {
return results;
}
for (const tsCodeFix of response.body) {
this.addAllFixesForTsCodeAction(results, document, file, diagnostic, tsCodeFix as Proto.CodeFixAction);
}
return results;
}
private addAllFixesForTsCodeAction(
results: CodeActionSet,
document: vscode.TextDocument,
file: string,
diagnostic: vscode.Diagnostic,
tsAction: Proto.CodeFixAction
): CodeActionSet {
results.addAction(this.getSingleFixForTsCodeAction(diagnostic, tsAction));
this.addFixAllForTsCodeAction(results, document, file, diagnostic, tsAction as Proto.CodeFixAction);
return results;
}
private getSingleFixForTsCodeAction(
diagnostic: vscode.Diagnostic,
tsAction: Proto.CodeFixAction
): VsCodeCodeAction {
const codeAction = new VsCodeCodeAction(tsAction, tsAction.description, vscode.CodeActionKind.QuickFix, false);
codeAction.edit = getEditForCodeAction(this.client, tsAction);
codeAction.diagnostics = [diagnostic];
codeAction.command = {
command: ApplyCodeActionCommand.ID,
arguments: [tsAction],
title: ''
};
return codeAction;
}
private addFixAllForTsCodeAction(
results: CodeActionSet,
document: vscode.TextDocument,
file: string,
diagnostic: vscode.Diagnostic,
tsAction: Proto.CodeFixAction,
): CodeActionSet {
if (!tsAction.fixId || this.client.apiVersion.lt(API.v270) || results.hasFixAllAction(tsAction.fixId)) {
return results;
}
// Make sure there are multiple diagnostics of the same type in the file
if (!this.diagnosticsManager.getDiagnostics(document.uri).some(x => {
if (x === diagnostic) {
return false;
}
return x.code === diagnostic.code
|| (fixAllErrorCodes.has(x.code as number) && fixAllErrorCodes.get(x.code as number) === fixAllErrorCodes.get(diagnostic.code as number));
})) {
return results;
}
const action = new VsCodeCodeAction(
tsAction,
tsAction.fixAllDescription || localize('fixAllInFileLabel', '{0} (Fix all in file)', tsAction.description),
vscode.CodeActionKind.QuickFix, true);
action.diagnostics = [diagnostic];
action.command = {
command: ApplyFixAllCodeAction.ID,
arguments: [file, tsAction],
title: ''
};
results.addFixAllAction(tsAction.fixId, action);
return results;
}
}
// Some fix all actions can actually fix multiple differnt diagnostics. Make sure we still show the fix all action
// in such cases
const fixAllErrorCodes = new Map<number, number>([
// Missing async
[2339, 2339],
[2345, 2339],
]);
const preferredFixes = new Map<string, { readonly value: number, readonly thereCanOnlyBeOne?: boolean }>([
[fixNames.annotateWithTypeFromJSDoc, { value: 1 }],
[fixNames.constructorForDerivedNeedSuperCall, { value: 1 }],
[fixNames.extendsInterfaceBecomesImplements, { value: 1 }],
[fixNames.awaitInSyncFunction, { value: 1 }],
[fixNames.classIncorrectlyImplementsInterface, { value: 3 }],
[fixNames.classDoesntImplementInheritedAbstractMember, { value: 3 }],
[fixNames.unreachableCode, { value: 1 }],
[fixNames.unusedIdentifier, { value: 1 }],
[fixNames.forgottenThisPropertyAccess, { value: 1 }],
[fixNames.spelling, { value: 2 }],
[fixNames.addMissingAwait, { value: 1 }],
[fixNames.fixImport, { value: 0, thereCanOnlyBeOne: true }],
]);
function isPreferredFix(
action: VsCodeCodeAction,
allActions: readonly VsCodeCodeAction[]
): boolean {
if (action.isFixAll) {
return false;
}
const fixPriority = preferredFixes.get(action.tsAction.fixName);
if (!fixPriority) {
return false;
}
return allActions.every(otherAction => {
if (otherAction === action) {
return true;
}
if (otherAction.isFixAll) {
return true;
}
const otherFixPriority = preferredFixes.get(otherAction.tsAction.fixName);
if (!otherFixPriority || otherFixPriority.value < fixPriority.value) {
return true;
} else if (otherFixPriority.value > fixPriority.value) {
return false;
}
if (fixPriority.thereCanOnlyBeOne && action.tsAction.fixName === otherAction.tsAction.fixName) {
return false;
}
return true;
});
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
fileConfigurationManager: FileConfigurationManager,
commandManager: CommandManager,
diagnosticsManager: DiagnosticsManager,
telemetryReporter: TelemetryReporter
) {
return conditionalRegistration([
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
return vscode.languages.registerCodeActionsProvider(selector.semantic,
new TypeScriptQuickFixProvider(client, fileConfigurationManager, commandManager, diagnosticsManager, telemetryReporter),
TypeScriptQuickFixProvider.metadata);
});
}

View File

@@ -0,0 +1,502 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { Command, CommandManager } from '../commands/commandManager';
import { LearnMoreAboutRefactoringsCommand } from '../commands/learnMoreAboutRefactorings';
import type * as Proto from '../protocol';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { nulToken } from '../utils/cancellation';
import { conditionalRegistration, requireMinVersion, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as fileSchemes from '../utils/fileSchemes';
import { TelemetryReporter } from '../utils/telemetry';
import * as typeConverters from '../utils/typeConverters';
import FormattingOptionsManager from './fileConfigurationManager';
const localize = nls.loadMessageBundle();
namespace Experimental {
export interface RefactorActionInfo extends Proto.RefactorActionInfo {
readonly notApplicableReason?: string;
}
}
interface DidApplyRefactoringCommand_Args {
readonly codeAction: InlinedCodeAction
}
class DidApplyRefactoringCommand implements Command {
public static readonly ID = '_typescript.didApplyRefactoring';
public readonly id = DidApplyRefactoringCommand.ID;
constructor(
private readonly telemetryReporter: TelemetryReporter
) { }
public async execute(args: DidApplyRefactoringCommand_Args): Promise<void> {
/* __GDPR__
"refactor.execute" : {
"action" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" },
"${include}": [
"${TypeScriptCommonProperties}"
]
}
*/
this.telemetryReporter.logTelemetry('refactor.execute', {
action: args.codeAction.action,
});
if (!args.codeAction.edit?.size) {
vscode.window.showErrorMessage(localize('refactoringFailed', "Could not apply refactoring"));
return;
}
const renameLocation = args.codeAction.renameLocation;
if (renameLocation) {
await vscode.commands.executeCommand('editor.action.rename', [
args.codeAction.document.uri,
typeConverters.Position.fromLocation(renameLocation)
]);
}
}
}
interface SelectRefactorCommand_Args {
readonly action: vscode.CodeAction;
readonly document: vscode.TextDocument;
readonly info: Proto.ApplicableRefactorInfo;
readonly rangeOrSelection: vscode.Range | vscode.Selection;
}
class SelectRefactorCommand implements Command {
public static readonly ID = '_typescript.selectRefactoring';
public readonly id = SelectRefactorCommand.ID;
constructor(
private readonly client: ITypeScriptServiceClient,
private readonly didApplyCommand: DidApplyRefactoringCommand
) { }
public async execute(args: SelectRefactorCommand_Args): Promise<void> {
const file = this.client.toOpenedFilePath(args.document);
if (!file) {
return;
}
const selected = await vscode.window.showQuickPick(args.info.actions.map((action): vscode.QuickPickItem => ({
label: action.name,
description: action.description,
})));
if (!selected) {
return;
}
const tsAction = new InlinedCodeAction(this.client, args.action.title, args.action.kind, args.document, args.info.name, selected.label, args.rangeOrSelection);
await tsAction.resolve(nulToken);
if (tsAction.edit) {
if (!(await vscode.workspace.applyEdit(tsAction.edit))) {
vscode.window.showErrorMessage(localize('refactoringFailed', "Could not apply refactoring"));
return;
}
}
await this.didApplyCommand.execute({ codeAction: tsAction });
}
}
interface CodeActionKind {
readonly kind: vscode.CodeActionKind;
matches(refactor: Proto.RefactorActionInfo): boolean;
}
const Extract_Function = Object.freeze<CodeActionKind>({
kind: vscode.CodeActionKind.RefactorExtract.append('function'),
matches: refactor => refactor.name.startsWith('function_')
});
const Extract_Constant = Object.freeze<CodeActionKind>({
kind: vscode.CodeActionKind.RefactorExtract.append('constant'),
matches: refactor => refactor.name.startsWith('constant_')
});
const Extract_Type = Object.freeze<CodeActionKind>({
kind: vscode.CodeActionKind.RefactorExtract.append('type'),
matches: refactor => refactor.name.startsWith('Extract to type alias')
});
const Extract_Interface = Object.freeze<CodeActionKind>({
kind: vscode.CodeActionKind.RefactorExtract.append('interface'),
matches: refactor => refactor.name.startsWith('Extract to interface')
});
const Move_NewFile = Object.freeze<CodeActionKind>({
kind: vscode.CodeActionKind.Refactor.append('move').append('newFile'),
matches: refactor => refactor.name.startsWith('Move to a new file')
});
const Rewrite_Import = Object.freeze<CodeActionKind>({
kind: vscode.CodeActionKind.RefactorRewrite.append('import'),
matches: refactor => refactor.name.startsWith('Convert namespace import') || refactor.name.startsWith('Convert named imports')
});
const Rewrite_Export = Object.freeze<CodeActionKind>({
kind: vscode.CodeActionKind.RefactorRewrite.append('export'),
matches: refactor => refactor.name.startsWith('Convert default export') || refactor.name.startsWith('Convert named export')
});
const Rewrite_Arrow_Braces = Object.freeze<CodeActionKind>({
kind: vscode.CodeActionKind.RefactorRewrite.append('arrow').append('braces'),
matches: refactor => refactor.name.startsWith('Convert default export') || refactor.name.startsWith('Convert named export')
});
const Rewrite_Parameters_ToDestructured = Object.freeze<CodeActionKind>({
kind: vscode.CodeActionKind.RefactorRewrite.append('parameters').append('toDestructured'),
matches: refactor => refactor.name.startsWith('Convert parameters to destructured object')
});
const Rewrite_Property_GenerateAccessors = Object.freeze<CodeActionKind>({
kind: vscode.CodeActionKind.RefactorRewrite.append('property').append('generateAccessors'),
matches: refactor => refactor.name.startsWith('Generate \'get\' and \'set\' accessors')
});
const allKnownCodeActionKinds = [
Extract_Function,
Extract_Constant,
Extract_Type,
Extract_Interface,
Move_NewFile,
Rewrite_Import,
Rewrite_Export,
Rewrite_Arrow_Braces,
Rewrite_Parameters_ToDestructured,
Rewrite_Property_GenerateAccessors
];
class InlinedCodeAction extends vscode.CodeAction {
constructor(
public readonly client: ITypeScriptServiceClient,
title: string,
kind: vscode.CodeActionKind | undefined,
public readonly document: vscode.TextDocument,
public readonly refactor: string,
public readonly action: string,
public readonly range: vscode.Range,
) {
super(title, kind);
}
// Filled in during resolve
public renameLocation?: Proto.Location;
public async resolve(token: vscode.CancellationToken): Promise<undefined> {
const file = this.client.toOpenedFilePath(this.document);
if (!file) {
return;
}
const args: Proto.GetEditsForRefactorRequestArgs = {
...typeConverters.Range.toFileRangeRequestArgs(file, this.range),
refactor: this.refactor,
action: this.action,
};
const response = await this.client.execute('getEditsForRefactor', args, token);
if (response.type !== 'response' || !response.body) {
return;
}
// Resolve
this.edit = InlinedCodeAction.getWorkspaceEditForRefactoring(this.client, response.body);
this.renameLocation = response.body.renameLocation;
return;
}
private static getWorkspaceEditForRefactoring(
client: ITypeScriptServiceClient,
body: Proto.RefactorEditInfo,
): vscode.WorkspaceEdit {
const workspaceEdit = new vscode.WorkspaceEdit();
for (const edit of body.edits) {
const resource = client.toResource(edit.fileName);
if (resource.scheme === fileSchemes.file) {
workspaceEdit.createFile(resource, { ignoreIfExists: true });
}
}
typeConverters.WorkspaceEdit.withFileCodeEdits(workspaceEdit, client, body.edits);
return workspaceEdit;
}
}
class SelectCodeAction extends vscode.CodeAction {
constructor(
info: Proto.ApplicableRefactorInfo,
document: vscode.TextDocument,
rangeOrSelection: vscode.Range | vscode.Selection
) {
super(info.description, vscode.CodeActionKind.Refactor);
this.command = {
title: info.description,
command: SelectRefactorCommand.ID,
arguments: [<SelectRefactorCommand_Args>{ action: this, document, info, rangeOrSelection }]
};
}
}
type TsCodeAction = InlinedCodeAction | SelectCodeAction;
class TypeScriptRefactorProvider implements vscode.CodeActionProvider<TsCodeAction> {
public static readonly minVersion = API.v240;
constructor(
private readonly client: ITypeScriptServiceClient,
private readonly formattingOptionsManager: FormattingOptionsManager,
commandManager: CommandManager,
telemetryReporter: TelemetryReporter
) {
const didApplyRefactoringCommand = commandManager.register(new DidApplyRefactoringCommand(telemetryReporter));
commandManager.register(new SelectRefactorCommand(this.client, didApplyRefactoringCommand));
}
public static readonly metadata: vscode.CodeActionProviderMetadata = {
providedCodeActionKinds: [
vscode.CodeActionKind.Refactor,
...allKnownCodeActionKinds.map(x => x.kind),
],
documentation: [
{
kind: vscode.CodeActionKind.Refactor,
command: {
command: LearnMoreAboutRefactoringsCommand.id,
title: localize('refactor.documentation.title', "Learn more about JS/TS refactorings")
}
}
]
};
public async provideCodeActions(
document: vscode.TextDocument,
rangeOrSelection: vscode.Range | vscode.Selection,
context: vscode.CodeActionContext,
token: vscode.CancellationToken
): Promise<TsCodeAction[] | undefined> {
if (!this.shouldTrigger(rangeOrSelection, context)) {
return undefined;
}
if (!this.client.toOpenedFilePath(document)) {
return undefined;
}
const response = await this.client.interruptGetErr(() => {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return undefined;
}
this.formattingOptionsManager.ensureConfigurationForDocument(document, token);
const args: Proto.GetApplicableRefactorsRequestArgs = {
...typeConverters.Range.toFileRangeRequestArgs(file, rangeOrSelection),
triggerReason: this.toTsTriggerReason(context),
};
return this.client.execute('getApplicableRefactors', args, token);
});
if (response?.type !== 'response' || !response.body) {
return undefined;
}
const actions = this.convertApplicableRefactors(response.body, document, rangeOrSelection);
if (!context.only) {
return actions;
}
return this.pruneInvalidActions(this.appendInvalidActions(actions), context.only, /* numberOfInvalid = */ 5);
}
public async resolveCodeAction(
codeAction: TsCodeAction,
token: vscode.CancellationToken,
): Promise<TsCodeAction> {
if (codeAction instanceof InlinedCodeAction) {
await codeAction.resolve(token);
}
return codeAction;
}
private toTsTriggerReason(context: vscode.CodeActionContext): Proto.RefactorTriggerReason | undefined {
if (!context.only) {
return;
}
return 'invoked';
}
private convertApplicableRefactors(
body: Proto.ApplicableRefactorInfo[],
document: vscode.TextDocument,
rangeOrSelection: vscode.Range | vscode.Selection
): TsCodeAction[] {
const actions: TsCodeAction[] = [];
for (const info of body) {
if (info.inlineable === false) {
const codeAction = new SelectCodeAction(info, document, rangeOrSelection);
actions.push(codeAction);
} else {
for (const action of info.actions) {
actions.push(this.refactorActionToCodeAction(action, document, info, rangeOrSelection, info.actions));
}
}
}
return actions;
}
private refactorActionToCodeAction(
action: Experimental.RefactorActionInfo,
document: vscode.TextDocument,
info: Proto.ApplicableRefactorInfo,
rangeOrSelection: vscode.Range | vscode.Selection,
allActions: readonly Proto.RefactorActionInfo[],
): InlinedCodeAction {
const codeAction = new InlinedCodeAction(this.client, action.description, TypeScriptRefactorProvider.getKind(action), document, info.name, action.name, rangeOrSelection);
// https://github.com/microsoft/TypeScript/pull/37871
if (action.notApplicableReason) {
codeAction.disabled = { reason: action.notApplicableReason };
} else {
codeAction.command = {
title: action.description,
command: DidApplyRefactoringCommand.ID,
arguments: [<DidApplyRefactoringCommand_Args>{ codeAction }],
};
}
codeAction.isPreferred = TypeScriptRefactorProvider.isPreferred(action, allActions);
return codeAction;
}
private shouldTrigger(rangeOrSelection: vscode.Range | vscode.Selection, context: vscode.CodeActionContext) {
if (context.only && !vscode.CodeActionKind.Refactor.contains(context.only)) {
return false;
}
return rangeOrSelection instanceof vscode.Selection;
}
private static getKind(refactor: Proto.RefactorActionInfo) {
const match = allKnownCodeActionKinds.find(kind => kind.matches(refactor));
return match ? match.kind : vscode.CodeActionKind.Refactor;
}
private static isPreferred(
action: Proto.RefactorActionInfo,
allActions: readonly Proto.RefactorActionInfo[],
): boolean {
if (Extract_Constant.matches(action)) {
// Only mark the action with the lowest scope as preferred
const getScope = (name: string) => {
const scope = name.match(/scope_(\d)/)?.[1];
return scope ? +scope : undefined;
};
const scope = getScope(action.name);
if (typeof scope !== 'number') {
return false;
}
return allActions
.filter(otherAtion => otherAtion !== action && Extract_Constant.matches(otherAtion))
.every(otherAction => {
const otherScope = getScope(otherAction.name);
return typeof otherScope === 'number' ? scope < otherScope : true;
});
}
if (Extract_Type.matches(action) || Extract_Interface.matches(action)) {
return true;
}
return false;
}
private appendInvalidActions(actions: vscode.CodeAction[]): vscode.CodeAction[] {
if (this.client.apiVersion.gte(API.v400)) {
// Invalid actions come from TS server instead
return actions;
}
if (!actions.some(action => action.kind && Extract_Constant.kind.contains(action.kind))) {
const disabledAction = new vscode.CodeAction(
localize('extractConstant.disabled.title', "Extract to constant"),
Extract_Constant.kind);
disabledAction.disabled = {
reason: localize('extractConstant.disabled.reason', "The current selection cannot be extracted"),
};
disabledAction.isPreferred = true;
actions.push(disabledAction);
}
if (!actions.some(action => action.kind && Extract_Function.kind.contains(action.kind))) {
const disabledAction = new vscode.CodeAction(
localize('extractFunction.disabled.title', "Extract to function"),
Extract_Function.kind);
disabledAction.disabled = {
reason: localize('extractFunction.disabled.reason', "The current selection cannot be extracted"),
};
actions.push(disabledAction);
}
return actions;
}
private pruneInvalidActions(actions: vscode.CodeAction[], only?: vscode.CodeActionKind, numberOfInvalid?: number): vscode.CodeAction[] {
if (this.client.apiVersion.lt(API.v400)) {
// Older TS version don't return extra actions
return actions;
}
const availableActions: vscode.CodeAction[] = [];
const invalidCommonActions: vscode.CodeAction[] = [];
const invalidUncommonActions: vscode.CodeAction[] = [];
for (const action of actions) {
if (!action.disabled) {
availableActions.push(action);
continue;
}
// These are the common refactors that we should always show if applicable.
if (action.kind && (Extract_Constant.kind.contains(action.kind) || Extract_Function.kind.contains(action.kind))) {
invalidCommonActions.push(action);
continue;
}
// These are the remaining refactors that we can show if we haven't reached the max limit with just common refactors.
invalidUncommonActions.push(action);
}
const prioritizedActions: vscode.CodeAction[] = [];
prioritizedActions.push(...invalidCommonActions);
prioritizedActions.push(...invalidUncommonActions);
const topNInvalid = prioritizedActions.filter(action => !only || (action.kind && only.contains(action.kind))).slice(0, numberOfInvalid);
availableActions.push(...topNInvalid);
return availableActions;
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
formattingOptionsManager: FormattingOptionsManager,
commandManager: CommandManager,
telemetryReporter: TelemetryReporter,
) {
return conditionalRegistration([
requireMinVersion(client, TypeScriptRefactorProvider.minVersion),
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
return vscode.languages.registerCodeActionsProvider(selector.semantic,
new TypeScriptRefactorProvider(client, formattingOptionsManager, commandManager, telemetryReporter),
TypeScriptRefactorProvider.metadata);
});
}

View File

@@ -0,0 +1,56 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as typeConverters from '../utils/typeConverters';
class TypeScriptReferenceSupport implements vscode.ReferenceProvider {
public constructor(
private readonly client: ITypeScriptServiceClient) { }
public async provideReferences(
document: vscode.TextDocument,
position: vscode.Position,
options: vscode.ReferenceContext,
token: vscode.CancellationToken
): Promise<vscode.Location[]> {
const filepath = this.client.toOpenedFilePath(document);
if (!filepath) {
return [];
}
const args = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
const response = await this.client.execute('references', args, token);
if (response.type !== 'response' || !response.body) {
return [];
}
const result: vscode.Location[] = [];
for (const ref of response.body.refs) {
if (!options.includeDeclaration && ref.isDefinition) {
continue;
}
const url = this.client.toResource(ref.file);
const location = typeConverters.Location.fromTextSpan(url, ref);
result.push(location);
}
return result;
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient
) {
return conditionalRegistration([
requireSomeCapability(client, ClientCapability.EnhancedSyntax, ClientCapability.Semantic),
], () => {
return vscode.languages.registerReferenceProvider(selector.syntax,
new TypeScriptReferenceSupport(client));
});
}

View File

@@ -0,0 +1,152 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import type * as Proto from '../protocol';
import { ClientCapability, ITypeScriptServiceClient, ServerResponse } from '../typescriptService';
import API from '../utils/api';
import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as typeConverters from '../utils/typeConverters';
import FileConfigurationManager from './fileConfigurationManager';
const localize = nls.loadMessageBundle();
class TypeScriptRenameProvider implements vscode.RenameProvider {
public constructor(
private readonly client: ITypeScriptServiceClient,
private readonly fileConfigurationManager: FileConfigurationManager
) { }
public async prepareRename(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.Range | null> {
if (this.client.apiVersion.lt(API.v310)) {
return null;
}
const response = await this.execRename(document, position, token);
if (response?.type !== 'response' || !response.body) {
return null;
}
const renameInfo = response.body.info;
if (!renameInfo.canRename) {
return Promise.reject<vscode.Range>(renameInfo.localizedErrorMessage);
}
return typeConverters.Range.fromTextSpan(renameInfo.triggerSpan);
}
public async provideRenameEdits(
document: vscode.TextDocument,
position: vscode.Position,
newName: string,
token: vscode.CancellationToken
): Promise<vscode.WorkspaceEdit | null> {
const response = await this.execRename(document, position, token);
if (!response || response.type !== 'response' || !response.body) {
return null;
}
const renameInfo = response.body.info;
if (!renameInfo.canRename) {
return Promise.reject<vscode.WorkspaceEdit>(renameInfo.localizedErrorMessage);
}
if (renameInfo.fileToRename) {
const edits = await this.renameFile(renameInfo.fileToRename, newName, token);
if (edits) {
return edits;
} else {
return Promise.reject<vscode.WorkspaceEdit>(localize('fileRenameFail', "An error occurred while renaming file"));
}
}
return this.updateLocs(response.body.locs, newName);
}
public async execRename(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<ServerResponse.Response<Proto.RenameResponse> | undefined> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return undefined;
}
const args: Proto.RenameRequestArgs = {
...typeConverters.Position.toFileLocationRequestArgs(file, position),
findInStrings: false,
findInComments: false
};
return this.client.interruptGetErr(() => {
this.fileConfigurationManager.ensureConfigurationForDocument(document, token);
return this.client.execute('rename', args, token);
});
}
private updateLocs(
locations: ReadonlyArray<Proto.SpanGroup>,
newName: string
) {
const edit = new vscode.WorkspaceEdit();
for (const spanGroup of locations) {
const resource = this.client.toResource(spanGroup.file);
for (const textSpan of spanGroup.locs) {
edit.replace(resource, typeConverters.Range.fromTextSpan(textSpan),
(textSpan.prefixText || '') + newName + (textSpan.suffixText || ''));
}
}
return edit;
}
private async renameFile(
fileToRename: string,
newName: string,
token: vscode.CancellationToken,
): Promise<vscode.WorkspaceEdit | undefined> {
// Make sure we preserve file extension if none provided
if (!path.extname(newName)) {
newName += path.extname(fileToRename);
}
const dirname = path.dirname(fileToRename);
const newFilePath = path.join(dirname, newName);
const args: Proto.GetEditsForFileRenameRequestArgs & { file: string } = {
file: fileToRename,
oldFilePath: fileToRename,
newFilePath: newFilePath,
};
const response = await this.client.execute('getEditsForFileRename', args, token);
if (response.type !== 'response' || !response.body) {
return undefined;
}
const edits = typeConverters.WorkspaceEdit.fromFileCodeEdits(this.client, response.body);
edits.renameFile(vscode.Uri.file(fileToRename), vscode.Uri.file(newFilePath));
return edits;
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
fileConfigurationManager: FileConfigurationManager,
) {
return conditionalRegistration([
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
return vscode.languages.registerRenameProvider(selector.semantic,
new TypeScriptRenameProvider(client, fileConfigurationManager));
});
}

View File

@@ -0,0 +1,283 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// all constants are const
import { TokenEncodingConsts, TokenModifier, TokenType, VersionRequirement } from 'typescript-vscode-sh-plugin/lib/constants';
import * as vscode from 'vscode';
import * as Proto from '../protocol';
import { ClientCapability, ExecConfig, ITypeScriptServiceClient, ServerResponse } from '../typescriptService';
import API from '../utils/api';
import { conditionalRegistration, requireSomeCapability, requireMinVersion } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
const minTypeScriptVersion = API.fromVersionString(`${VersionRequirement.major}.${VersionRequirement.minor}`);
// as we don't do deltas, for performance reasons, don't compute semantic tokens for documents above that limit
const CONTENT_LENGTH_LIMIT = 100000;
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
) {
return conditionalRegistration([
requireMinVersion(client, minTypeScriptVersion),
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
const provider = new DocumentSemanticTokensProvider(client);
return vscode.Disposable.from(
// register only as a range provider
vscode.languages.registerDocumentRangeSemanticTokensProvider(selector.semantic, provider, provider.getLegend()),
);
});
}
/**
* 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.
*/
class DocumentSemanticTokensProvider implements vscode.DocumentSemanticTokensProvider, vscode.DocumentRangeSemanticTokensProvider {
constructor(private readonly client: ITypeScriptServiceClient) {
}
getLegend(): vscode.SemanticTokensLegend {
return new vscode.SemanticTokensLegend(tokenTypes, tokenModifiers);
}
async provideDocumentSemanticTokens(document: vscode.TextDocument, token: vscode.CancellationToken): Promise<vscode.SemanticTokens | null> {
const file = this.client.toOpenedFilePath(document);
if (!file || document.getText().length > CONTENT_LENGTH_LIMIT) {
return null;
}
return this._provideSemanticTokens(document, { file, start: 0, length: document.getText().length }, token);
}
async provideDocumentRangeSemanticTokens(document: vscode.TextDocument, range: vscode.Range, token: vscode.CancellationToken): Promise<vscode.SemanticTokens | null> {
const file = this.client.toOpenedFilePath(document);
if (!file || (document.offsetAt(range.end) - document.offsetAt(range.start) > CONTENT_LENGTH_LIMIT)) {
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: vscode.TextDocument, requestArg: ExperimentalProtocol.EncodedSemanticClassificationsRequestArgs, token: vscode.CancellationToken): Promise<vscode.SemanticTokens | null> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return null;
}
let versionBeforeRequest = document.version;
const response = await (this.client as ExperimentalProtocol.IExtendedTypeScriptServiceClient).execute('encodedSemanticClassifications-full', requestArg, token);
if (response.type !== 'response' || !response.body) {
return null;
}
const versionAfterRequest = document.version;
if (versionBeforeRequest !== versionAfterRequest) {
// cannot convert result's offsets to (line;col) 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('busy');
}
const tokenSpan = response.body.spans;
const builder = new vscode.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 : document.lineAt(line).text.length);
builder.push(line, startCharacter, endCharacter - startCharacter, tokenType, tokenModifiers);
}
}
return builder.build();
}
}
function waitForDocumentChangesToEnd(document: vscode.TextDocument) {
let version = document.version;
return new Promise<void>((s) => {
let iv = setInterval(_ => {
if (document.version === version) {
clearInterval(iv);
s();
}
version = document.version;
}, 400);
});
}
// typescript-vscode-sh-plugin encodes type and modifiers in the classification:
// TSClassification = (TokenType + 1) << 8 + TokenModifier
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;
}
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.member] = 'member';
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';
// make sure token types and modifiers are complete
if (tokenTypes.filter(t => !!t).length !== TokenType._) {
console.warn('typescript-vscode-sh-plugin has added new tokens types.');
}
if (tokenModifiers.filter(t => !!t).length !== TokenModifier._) {
console.warn('typescript-vscode-sh-plugin has added new tokens modifiers.');
}
// 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;
namespace ExperimentalProtocol {
export interface IExtendedTypeScriptServiceClient {
execute<K extends keyof ExperimentalProtocol.ExtendedTsServerRequests>(
command: K,
args: ExperimentalProtocol.ExtendedTsServerRequests[K][0],
token: vscode.CancellationToken,
config?: ExecConfig
): Promise<ServerResponse.Response<ExperimentalProtocol.ExtendedTsServerRequests[K][1]>>;
}
/**
* 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];
}
}

View File

@@ -0,0 +1,137 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as Previewer from '../utils/previewer';
import * as typeConverters from '../utils/typeConverters';
class TypeScriptSignatureHelpProvider implements vscode.SignatureHelpProvider {
public static readonly triggerCharacters = ['(', ',', '<'];
public static readonly retriggerCharacters = [')'];
public constructor(
private readonly client: ITypeScriptServiceClient
) { }
public async provideSignatureHelp(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken,
context: vscode.SignatureHelpContext,
): Promise<vscode.SignatureHelp | undefined> {
const filepath = this.client.toOpenedFilePath(document);
if (!filepath) {
return undefined;
}
const args: Proto.SignatureHelpRequestArgs = {
...typeConverters.Position.toFileLocationRequestArgs(filepath, position),
triggerReason: toTsTriggerReason(context)
};
const response = await this.client.interruptGetErr(() => this.client.execute('signatureHelp', args, token));
if (response.type !== 'response' || !response.body) {
return undefined;
}
const info = response.body;
const result = new vscode.SignatureHelp();
result.signatures = info.items.map(signature => this.convertSignature(signature));
result.activeSignature = this.getActiveSignature(context, info, result.signatures);
result.activeParameter = this.getActiveParameter(info);
return result;
}
private getActiveSignature(context: vscode.SignatureHelpContext, info: Proto.SignatureHelpItems, signatures: readonly vscode.SignatureInformation[]): number {
// Try matching the previous active signature's label to keep it selected
const previouslyActiveSignature = context.activeSignatureHelp?.signatures[context.activeSignatureHelp.activeSignature];
if (previouslyActiveSignature && context.isRetrigger) {
const existingIndex = signatures.findIndex(other => other.label === previouslyActiveSignature?.label);
if (existingIndex >= 0) {
return existingIndex;
}
}
return info.selectedItemIndex;
}
private getActiveParameter(info: Proto.SignatureHelpItems): number {
const activeSignature = info.items[info.selectedItemIndex];
if (activeSignature && activeSignature.isVariadic) {
return Math.min(info.argumentIndex, activeSignature.parameters.length - 1);
}
return info.argumentIndex;
}
private convertSignature(item: Proto.SignatureHelpItem) {
const signature = new vscode.SignatureInformation(
Previewer.plain(item.prefixDisplayParts),
Previewer.markdownDocumentation(item.documentation, item.tags.filter(x => x.name !== 'param')));
let textIndex = signature.label.length;
const separatorLabel = Previewer.plain(item.separatorDisplayParts);
for (let i = 0; i < item.parameters.length; ++i) {
const parameter = item.parameters[i];
const label = Previewer.plain(parameter.displayParts);
signature.parameters.push(
new vscode.ParameterInformation(
[textIndex, textIndex + label.length],
Previewer.markdownDocumentation(parameter.documentation, [])));
textIndex += label.length;
signature.label += label;
if (i !== item.parameters.length - 1) {
signature.label += separatorLabel;
textIndex += separatorLabel.length;
}
}
signature.label += Previewer.plain(item.suffixDisplayParts);
return signature;
}
}
function toTsTriggerReason(context: vscode.SignatureHelpContext): Proto.SignatureHelpTriggerReason {
switch (context.triggerKind) {
case vscode.SignatureHelpTriggerKind.TriggerCharacter:
if (context.triggerCharacter) {
if (context.isRetrigger) {
return { kind: 'retrigger', triggerCharacter: context.triggerCharacter as any };
} else {
return { kind: 'characterTyped', triggerCharacter: context.triggerCharacter as any };
}
} else {
return { kind: 'invoked' };
}
case vscode.SignatureHelpTriggerKind.ContentChange:
return context.isRetrigger ? { kind: 'retrigger' } : { kind: 'invoked' };
case vscode.SignatureHelpTriggerKind.Invoke:
default:
return { kind: 'invoked' };
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
) {
return conditionalRegistration([
requireSomeCapability(client, ClientCapability.EnhancedSyntax, ClientCapability.Semantic),
], () => {
return vscode.languages.registerSignatureHelpProvider(selector.syntax,
new TypeScriptSignatureHelpProvider(client), {
triggerCharacters: TypeScriptSignatureHelpProvider.triggerCharacters,
retriggerCharacters: TypeScriptSignatureHelpProvider.retriggerCharacters
});
});
}

View File

@@ -0,0 +1,61 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { conditionalRegistration, requireMinVersion } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import * as typeConverters from '../utils/typeConverters';
class SmartSelection implements vscode.SelectionRangeProvider {
public static readonly minVersion = API.v350;
public constructor(
private readonly client: ITypeScriptServiceClient
) { }
public async provideSelectionRanges(
document: vscode.TextDocument,
positions: vscode.Position[],
token: vscode.CancellationToken,
): Promise<vscode.SelectionRange[] | undefined> {
const file = this.client.toOpenedFilePath(document);
if (!file) {
return undefined;
}
const args: Proto.SelectionRangeRequestArgs = {
file,
locations: positions.map(typeConverters.Position.toLocation)
};
const response = await this.client.execute('selectionRange', args, token);
if (response.type !== 'response' || !response.body) {
return undefined;
}
return response.body.map(SmartSelection.convertSelectionRange);
}
private static convertSelectionRange(
selectionRange: Proto.SelectionRange
): vscode.SelectionRange {
return new vscode.SelectionRange(
typeConverters.Range.fromTextSpan(selectionRange.textSpan),
selectionRange.parent ? SmartSelection.convertSelectionRange(selectionRange.parent) : undefined,
);
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
) {
return conditionalRegistration([
requireMinVersion(client, SmartSelection.minVersion),
], () => {
return vscode.languages.registerSelectionRangeProvider(selector.syntax, new SmartSelection(client));
});
}

View File

@@ -0,0 +1,164 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { conditionalRegistration, requireMinVersion, requireConfiguration, Condition } from '../utils/dependentRegistration';
import { Disposable } from '../utils/dispose';
import { DocumentSelector } from '../utils/documentSelector';
import * as typeConverters from '../utils/typeConverters';
class TagClosing extends Disposable {
public static readonly minVersion = API.v300;
private _disposed = false;
private _timeout: NodeJS.Timer | undefined = undefined;
private _cancel: vscode.CancellationTokenSource | undefined = undefined;
constructor(
private readonly client: ITypeScriptServiceClient
) {
super();
vscode.workspace.onDidChangeTextDocument(
event => this.onDidChangeTextDocument(event.document, event.contentChanges),
null,
this._disposables);
}
public dispose() {
super.dispose();
this._disposed = true;
if (this._timeout) {
clearTimeout(this._timeout);
this._timeout = undefined;
}
if (this._cancel) {
this._cancel.cancel();
this._cancel.dispose();
this._cancel = undefined;
}
}
private onDidChangeTextDocument(
document: vscode.TextDocument,
changes: readonly vscode.TextDocumentContentChangeEvent[]
) {
const activeDocument = vscode.window.activeTextEditor && vscode.window.activeTextEditor.document;
if (document !== activeDocument || changes.length === 0) {
return;
}
const filepath = this.client.toOpenedFilePath(document);
if (!filepath) {
return;
}
if (typeof this._timeout !== 'undefined') {
clearTimeout(this._timeout);
}
if (this._cancel) {
this._cancel.cancel();
this._cancel.dispose();
this._cancel = undefined;
}
const lastChange = changes[changes.length - 1];
const lastCharacter = lastChange.text[lastChange.text.length - 1];
if (lastChange.rangeLength > 0 || lastCharacter !== '>' && lastCharacter !== '/') {
return;
}
const priorCharacter = lastChange.range.start.character > 0
? document.getText(new vscode.Range(lastChange.range.start.translate({ characterDelta: -1 }), lastChange.range.start))
: '';
if (priorCharacter === '>') {
return;
}
const version = document.version;
this._timeout = setTimeout(async () => {
this._timeout = undefined;
if (this._disposed) {
return;
}
const addedLines = lastChange.text.split(/\r\n|\n/g);
const position = addedLines.length <= 1
? lastChange.range.start.translate({ characterDelta: lastChange.text.length })
: new vscode.Position(lastChange.range.start.line + addedLines.length - 1, addedLines[addedLines.length - 1].length);
const args: Proto.JsxClosingTagRequestArgs = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
this._cancel = new vscode.CancellationTokenSource();
const response = await this.client.execute('jsxClosingTag', args, this._cancel.token);
if (response.type !== 'response' || !response.body) {
return;
}
if (this._disposed) {
return;
}
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
return;
}
const insertion = response.body;
const activeDocument = activeEditor.document;
if (document === activeDocument && activeDocument.version === version) {
activeEditor.insertSnippet(
this.getTagSnippet(insertion),
this.getInsertionPositions(activeEditor, position));
}
}, 100);
}
private getTagSnippet(closingTag: Proto.TextInsertion): vscode.SnippetString {
const snippet = new vscode.SnippetString();
snippet.appendPlaceholder('', 0);
snippet.appendText(closingTag.newText);
return snippet;
}
private getInsertionPositions(editor: vscode.TextEditor, position: vscode.Position) {
const activeSelectionPositions = editor.selections.map(s => s.active);
return activeSelectionPositions.some(p => p.isEqual(position))
? activeSelectionPositions
: position;
}
}
function requireActiveDocument(
selector: vscode.DocumentSelector
) {
return new Condition(
() => {
const editor = vscode.window.activeTextEditor;
return !!(editor && vscode.languages.match(selector, editor.document));
},
handler => {
return vscode.Disposable.from(
vscode.window.onDidChangeActiveTextEditor(handler),
vscode.workspace.onDidOpenTextDocument(handler));
});
}
export function register(
selector: DocumentSelector,
modeId: string,
client: ITypeScriptServiceClient,
) {
return conditionalRegistration([
requireMinVersion(client, TagClosing.minVersion),
requireConfiguration(modeId, 'autoClosingTags'),
requireActiveDocument(selector.syntax)
], () => new TagClosing(client));
}

View File

@@ -0,0 +1,122 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as jsonc from 'jsonc-parser';
import { basename, dirname, join } from 'path';
import * as vscode from 'vscode';
import { coalesce, flatten } from '../utils/arrays';
function mapChildren<R>(node: jsonc.Node | undefined, f: (x: jsonc.Node) => R): R[] {
return node && node.type === 'array' && node.children
? node.children.map(f)
: [];
}
class TsconfigLinkProvider implements vscode.DocumentLinkProvider {
public provideDocumentLinks(
document: vscode.TextDocument,
_token: vscode.CancellationToken
): vscode.ProviderResult<vscode.DocumentLink[]> {
const root = jsonc.parseTree(document.getText());
if (!root) {
return null;
}
return coalesce([
this.getExtendsLink(document, root),
...this.getFilesLinks(document, root),
...this.getReferencesLinks(document, root)
]);
}
private getExtendsLink(document: vscode.TextDocument, root: jsonc.Node): vscode.DocumentLink | undefined {
const extendsNode = jsonc.findNodeAtLocation(root, ['extends']);
if (!this.isPathValue(extendsNode)) {
return undefined;
}
if (extendsNode.value.startsWith('.')) {
return new vscode.DocumentLink(
this.getRange(document, extendsNode),
vscode.Uri.file(join(dirname(document.uri.fsPath), extendsNode.value + (extendsNode.value.endsWith('.json') ? '' : '.json')))
);
}
const workspaceFolderPath = vscode.workspace.getWorkspaceFolder(document.uri)!.uri.fsPath;
return new vscode.DocumentLink(
this.getRange(document, extendsNode),
vscode.Uri.file(join(workspaceFolderPath, 'node_modules', extendsNode.value + (extendsNode.value.endsWith('.json') ? '' : '.json')))
);
}
private getFilesLinks(document: vscode.TextDocument, root: jsonc.Node) {
return mapChildren(
jsonc.findNodeAtLocation(root, ['files']),
child => this.pathNodeToLink(document, child));
}
private getReferencesLinks(document: vscode.TextDocument, root: jsonc.Node) {
return mapChildren(
jsonc.findNodeAtLocation(root, ['references']),
child => {
const pathNode = jsonc.findNodeAtLocation(child, ['path']);
if (!this.isPathValue(pathNode)) {
return undefined;
}
return new vscode.DocumentLink(this.getRange(document, pathNode),
basename(pathNode.value).endsWith('.json')
? this.getFileTarget(document, pathNode)
: this.getFolderTarget(document, pathNode));
});
}
private pathNodeToLink(
document: vscode.TextDocument,
node: jsonc.Node | undefined
): vscode.DocumentLink | undefined {
return this.isPathValue(node)
? new vscode.DocumentLink(this.getRange(document, node), this.getFileTarget(document, node))
: undefined;
}
private isPathValue(extendsNode: jsonc.Node | undefined): extendsNode is jsonc.Node {
return extendsNode
&& extendsNode.type === 'string'
&& extendsNode.value
&& !(extendsNode.value as string).includes('*'); // don't treat globs as links.
}
private getFileTarget(document: vscode.TextDocument, node: jsonc.Node): vscode.Uri {
return vscode.Uri.file(join(dirname(document.uri.fsPath), node!.value));
}
private getFolderTarget(document: vscode.TextDocument, node: jsonc.Node): vscode.Uri {
return vscode.Uri.file(join(dirname(document.uri.fsPath), node!.value, 'tsconfig.json'));
}
private getRange(document: vscode.TextDocument, node: jsonc.Node) {
const offset = node!.offset;
const start = document.positionAt(offset + 1);
const end = document.positionAt(offset + (node!.length - 1));
return new vscode.Range(start, end);
}
}
export function register() {
const patterns: vscode.GlobPattern[] = [
'**/[jt]sconfig.json',
'**/[jt]sconfig.*.json',
];
const languages = ['json', 'jsonc'];
const selector: vscode.DocumentSelector = flatten(
languages.map(language =>
patterns.map((pattern): vscode.DocumentFilter => ({ language, pattern }))));
return vscode.languages.registerDocumentLinkProvider(selector, new TsconfigLinkProvider());
}

View File

@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration';
import { DocumentSelector } from '../utils/documentSelector';
import DefinitionProviderBase from './definitionProviderBase';
export default class TypeScriptTypeDefinitionProvider extends DefinitionProviderBase implements vscode.TypeDefinitionProvider {
public provideTypeDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Definition | undefined> {
return this.getSymbolLocations('typeDefinition', document, position, token);
}
}
export function register(
selector: DocumentSelector,
client: ITypeScriptServiceClient,
) {
return conditionalRegistration([
requireSomeCapability(client, ClientCapability.EnhancedSyntax, ClientCapability.Semantic),
], () => {
return vscode.languages.registerTypeDefinitionProvider(selector.syntax,
new TypeScriptTypeDefinitionProvider(client));
});
}

View File

@@ -0,0 +1,303 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import type * as Proto from '../protocol';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { Delayer } from '../utils/async';
import { nulToken } from '../utils/cancellation';
import { conditionalRegistration, requireSomeCapability, requireMinVersion } from '../utils/dependentRegistration';
import { Disposable } from '../utils/dispose';
import * as fileSchemes from '../utils/fileSchemes';
import { doesResourceLookLikeATypeScriptFile } from '../utils/languageDescription';
import * as typeConverters from '../utils/typeConverters';
import FileConfigurationManager from './fileConfigurationManager';
const localize = nls.loadMessageBundle();
const updateImportsOnFileMoveName = 'updateImportsOnFileMove.enabled';
async function isDirectory(resource: vscode.Uri): Promise<boolean> {
try {
return (await vscode.workspace.fs.stat(resource)).type === vscode.FileType.Directory;
} catch {
return false;
}
}
const enum UpdateImportsOnFileMoveSetting {
Prompt = 'prompt',
Always = 'always',
Never = 'never',
}
interface RenameAction {
readonly oldUri: vscode.Uri;
readonly newUri: vscode.Uri;
readonly newFilePath: string;
readonly oldFilePath: string;
readonly jsTsFileThatIsBeingMoved: vscode.Uri;
}
class UpdateImportsOnFileRenameHandler extends Disposable {
public static readonly minVersion = API.v300;
private readonly _delayer = new Delayer(50);
private readonly _pendingRenames = new Set<RenameAction>();
public constructor(
private readonly client: ITypeScriptServiceClient,
private readonly fileConfigurationManager: FileConfigurationManager,
private readonly _handles: (uri: vscode.Uri) => Promise<boolean>,
) {
super();
this._register(vscode.workspace.onDidRenameFiles(async (e) => {
const [{ newUri, oldUri }] = e.files;
const newFilePath = this.client.toPath(newUri);
if (!newFilePath) {
return;
}
const oldFilePath = this.client.toPath(oldUri);
if (!oldFilePath) {
return;
}
const config = this.getConfiguration(newUri);
const setting = config.get<UpdateImportsOnFileMoveSetting>(updateImportsOnFileMoveName);
if (setting === UpdateImportsOnFileMoveSetting.Never) {
return;
}
// Try to get a js/ts file that is being moved
// For directory moves, this returns a js/ts file under the directory.
const jsTsFileThatIsBeingMoved = await this.getJsTsFileBeingMoved(newUri);
if (!jsTsFileThatIsBeingMoved || !this.client.toPath(jsTsFileThatIsBeingMoved)) {
return;
}
this._pendingRenames.add({ oldUri, newUri, newFilePath, oldFilePath, jsTsFileThatIsBeingMoved });
this._delayer.trigger(() => {
vscode.window.withProgress({
location: vscode.ProgressLocation.Window,
title: localize('renameProgress.title', "Checking for update of JS/TS imports")
}, () => this.flushRenames());
});
}));
}
private async flushRenames(): Promise<void> {
const renames = Array.from(this._pendingRenames);
this._pendingRenames.clear();
for (const group of this.groupRenames(renames)) {
const edits = new vscode.WorkspaceEdit();
const resourcesBeingRenamed: vscode.Uri[] = [];
for (const { oldUri, newUri, newFilePath, oldFilePath, jsTsFileThatIsBeingMoved } of group) {
const document = await vscode.workspace.openTextDocument(jsTsFileThatIsBeingMoved);
// Make sure TS knows about file
this.client.bufferSyncSupport.closeResource(oldUri);
this.client.bufferSyncSupport.openTextDocument(document);
if (await this.withEditsForFileRename(edits, document, oldFilePath, newFilePath)) {
resourcesBeingRenamed.push(newUri);
}
}
if (edits.size) {
if (await this.confirmActionWithUser(resourcesBeingRenamed)) {
await vscode.workspace.applyEdit(edits);
}
}
}
}
private async confirmActionWithUser(newResources: readonly vscode.Uri[]): Promise<boolean> {
if (!newResources.length) {
return false;
}
const config = this.getConfiguration(newResources[0]);
const setting = config.get<UpdateImportsOnFileMoveSetting>(updateImportsOnFileMoveName);
switch (setting) {
case UpdateImportsOnFileMoveSetting.Always:
return true;
case UpdateImportsOnFileMoveSetting.Never:
return false;
case UpdateImportsOnFileMoveSetting.Prompt:
default:
return this.promptUser(newResources);
}
}
private getConfiguration(resource: vscode.Uri) {
return vscode.workspace.getConfiguration(doesResourceLookLikeATypeScriptFile(resource) ? 'typescript' : 'javascript', resource);
}
private async promptUser(newResources: readonly vscode.Uri[]): Promise<boolean> {
if (!newResources.length) {
return false;
}
const enum Choice {
None = 0,
Accept = 1,
Reject = 2,
Always = 3,
Never = 4,
}
interface Item extends vscode.MessageItem {
readonly choice: Choice;
}
const response = await vscode.window.showInformationMessage<Item>(
newResources.length === 1
? localize('prompt', "Update imports for '{0}'?", path.basename(newResources[0].fsPath))
: this.getConfirmMessage(localize('promptMoreThanOne', "Update imports for the following {0} files?", newResources.length), newResources), {
modal: true,
}, {
title: localize('reject.title', "No"),
choice: Choice.Reject,
isCloseAffordance: true,
}, {
title: localize('accept.title', "Yes"),
choice: Choice.Accept,
}, {
title: localize('always.title', "Always automatically update imports"),
choice: Choice.Always,
}, {
title: localize('never.title', "Never automatically update imports"),
choice: Choice.Never,
});
if (!response) {
return false;
}
switch (response.choice) {
case Choice.Accept:
{
return true;
}
case Choice.Reject:
{
return false;
}
case Choice.Always:
{
const config = this.getConfiguration(newResources[0]);
config.update(
updateImportsOnFileMoveName,
UpdateImportsOnFileMoveSetting.Always,
vscode.ConfigurationTarget.Global);
return true;
}
case Choice.Never:
{
const config = this.getConfiguration(newResources[0]);
config.update(
updateImportsOnFileMoveName,
UpdateImportsOnFileMoveSetting.Never,
vscode.ConfigurationTarget.Global);
return false;
}
}
return false;
}
private async getJsTsFileBeingMoved(resource: vscode.Uri): Promise<vscode.Uri | undefined> {
if (resource.scheme !== fileSchemes.file) {
return undefined;
}
if (await isDirectory(resource)) {
const files = await vscode.workspace.findFiles({
base: resource.fsPath,
pattern: '**/*.{ts,tsx,js,jsx}',
}, '**/node_modules/**', 1);
return files[0];
}
return (await this._handles(resource)) ? resource : undefined;
}
private async withEditsForFileRename(
edits: vscode.WorkspaceEdit,
document: vscode.TextDocument,
oldFilePath: string,
newFilePath: string,
): Promise<boolean> {
const response = await this.client.interruptGetErr(() => {
this.fileConfigurationManager.setGlobalConfigurationFromDocument(document, nulToken);
const args: Proto.GetEditsForFileRenameRequestArgs = {
oldFilePath,
newFilePath,
};
return this.client.execute('getEditsForFileRename', args, nulToken);
});
if (response.type !== 'response' || !response.body.length) {
return false;
}
typeConverters.WorkspaceEdit.withFileCodeEdits(edits, this.client, response.body);
return true;
}
private groupRenames(renames: Iterable<RenameAction>): Iterable<Iterable<RenameAction>> {
const groups = new Map<string, Set<RenameAction>>();
for (const rename of renames) {
// Group renames by type (js/ts) and by workspace.
const key = `${this.client.getWorkspaceRootForResource(rename.jsTsFileThatIsBeingMoved)}@@@${doesResourceLookLikeATypeScriptFile(rename.jsTsFileThatIsBeingMoved)}`;
if (!groups.has(key)) {
groups.set(key, new Set());
}
groups.get(key)!.add(rename);
}
return groups.values();
}
private getConfirmMessage(start: string, resourcesToConfirm: readonly vscode.Uri[]): string {
const MAX_CONFIRM_FILES = 10;
const paths = [start];
paths.push('');
paths.push(...resourcesToConfirm.slice(0, MAX_CONFIRM_FILES).map(r => path.basename(r.fsPath)));
if (resourcesToConfirm.length > MAX_CONFIRM_FILES) {
if (resourcesToConfirm.length - MAX_CONFIRM_FILES === 1) {
paths.push(localize('moreFile', "...1 additional file not shown"));
} else {
paths.push(localize('moreFiles', "...{0} additional files not shown", resourcesToConfirm.length - MAX_CONFIRM_FILES));
}
}
paths.push('');
return paths.join('\n');
}
}
export function register(
client: ITypeScriptServiceClient,
fileConfigurationManager: FileConfigurationManager,
handles: (uri: vscode.Uri) => Promise<boolean>,
) {
return conditionalRegistration([
requireMinVersion(client, UpdateImportsOnFileRenameHandler.minVersion),
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
return new UpdateImportsOnFileRenameHandler(client, fileConfigurationManager, handles);
});
}

View File

@@ -0,0 +1,142 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import * as PConst from '../protocol.const';
import { ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import * as fileSchemes from '../utils/fileSchemes';
import { doesResourceLookLikeAJavaScriptFile, doesResourceLookLikeATypeScriptFile } from '../utils/languageDescription';
import * as typeConverters from '../utils/typeConverters';
import { parseKindModifier } from '../utils/modifiers';
function getSymbolKind(item: Proto.NavtoItem): vscode.SymbolKind {
switch (item.kind) {
case PConst.Kind.method: return vscode.SymbolKind.Method;
case PConst.Kind.enum: return vscode.SymbolKind.Enum;
case PConst.Kind.enumMember: return vscode.SymbolKind.EnumMember;
case PConst.Kind.function: return vscode.SymbolKind.Function;
case PConst.Kind.class: return vscode.SymbolKind.Class;
case PConst.Kind.interface: return vscode.SymbolKind.Interface;
case PConst.Kind.type: return vscode.SymbolKind.Class;
case PConst.Kind.memberVariable: return vscode.SymbolKind.Field;
case PConst.Kind.memberGetAccessor: return vscode.SymbolKind.Field;
case PConst.Kind.memberSetAccessor: return vscode.SymbolKind.Field;
case PConst.Kind.variable: return vscode.SymbolKind.Variable;
default: return vscode.SymbolKind.Variable;
}
}
class TypeScriptWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider {
public constructor(
private readonly client: ITypeScriptServiceClient,
private readonly modeIds: readonly string[],
) { }
public async provideWorkspaceSymbols(
search: string,
token: vscode.CancellationToken
): Promise<vscode.SymbolInformation[]> {
let file: string | undefined;
if (this.searchAllOpenProjects) {
file = undefined;
} else {
const document = this.getDocument();
file = document ? await this.toOpenedFiledPath(document) : undefined;
if (!file && this.client.apiVersion.lt(API.v390)) {
return [];
}
}
const args: Proto.NavtoRequestArgs = {
file,
searchValue: search,
maxResultCount: 256,
};
const response = await this.client.execute('navto', args, token);
if (response.type !== 'response' || !response.body) {
return [];
}
return response.body
.filter(item => item.containerName || item.kind !== 'alias')
.map(item => this.toSymbolInformation(item));
}
private get searchAllOpenProjects() {
return this.client.apiVersion.gte(API.v390)
&& vscode.workspace.getConfiguration('typescript').get('workspaceSymbols.scope', 'allOpenProjects') === 'allOpenProjects';
}
private async toOpenedFiledPath(document: vscode.TextDocument) {
if (document.uri.scheme === fileSchemes.git) {
try {
const path = vscode.Uri.file(JSON.parse(document.uri.query)?.path);
if (doesResourceLookLikeATypeScriptFile(path) || doesResourceLookLikeAJavaScriptFile(path)) {
const document = await vscode.workspace.openTextDocument(path);
return this.client.toOpenedFilePath(document);
}
} catch {
// noop
}
}
return this.client.toOpenedFilePath(document);
}
private toSymbolInformation(item: Proto.NavtoItem) {
const label = TypeScriptWorkspaceSymbolProvider.getLabel(item);
const info = new vscode.SymbolInformation(
label,
getSymbolKind(item),
item.containerName || '',
typeConverters.Location.fromTextSpan(this.client.toResource(item.file), item));
const kindModifiers = item.kindModifiers ? parseKindModifier(item.kindModifiers) : undefined;
if (kindModifiers?.has(PConst.KindModifiers.depreacted)) {
info.tags = [vscode.SymbolTag.Deprecated];
}
return info;
}
private static getLabel(item: Proto.NavtoItem) {
const label = item.name;
if (item.kind === 'method' || item.kind === 'function') {
return label + '()';
}
return label;
}
private getDocument(): vscode.TextDocument | undefined {
// typescript wants to have a resource even when asking
// general questions so we check the active editor. If this
// doesn't match we take the first TS document.
const activeDocument = vscode.window.activeTextEditor?.document;
if (activeDocument) {
if (this.modeIds.includes(activeDocument.languageId)) {
return activeDocument;
}
}
const documents = vscode.workspace.textDocuments;
for (const document of documents) {
if (this.modeIds.includes(document.languageId)) {
return document;
}
}
return undefined;
}
}
export function register(
client: ITypeScriptServiceClient,
modeIds: readonly string[],
) {
return vscode.languages.registerWorkspaceSymbolProvider(
new TypeScriptWorkspaceSymbolProvider(client, modeIds));
}

View File

@@ -0,0 +1,154 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { basename } from 'path';
import * as vscode from 'vscode';
import { DiagnosticKind } from './languageFeatures/diagnostics';
import FileConfigurationManager from './languageFeatures/fileConfigurationManager';
import { CachedResponse } from './tsServer/cachedResponse';
import TypeScriptServiceClient from './typescriptServiceClient';
import { CommandManager } from './commands/commandManager';
import { Disposable } from './utils/dispose';
import { DocumentSelector } from './utils/documentSelector';
import * as fileSchemes from './utils/fileSchemes';
import { LanguageDescription } from './utils/languageDescription';
import { TelemetryReporter } from './utils/telemetry';
import TypingsStatus from './utils/typingsStatus';
const validateSetting = 'validate.enable';
const suggestionSetting = 'suggestionActions.enabled';
export default class LanguageProvider extends Disposable {
constructor(
private readonly client: TypeScriptServiceClient,
private readonly description: LanguageDescription,
private readonly commandManager: CommandManager,
private readonly telemetryReporter: TelemetryReporter,
private readonly typingsStatus: TypingsStatus,
private readonly fileConfigurationManager: FileConfigurationManager,
private readonly onCompletionAccepted: (item: vscode.CompletionItem) => void,
) {
super();
vscode.workspace.onDidChangeConfiguration(this.configurationChanged, this, this._disposables);
this.configurationChanged();
client.onReady(() => this.registerProviders());
}
private get documentSelector(): DocumentSelector {
const semantic: vscode.DocumentFilter[] = [];
const syntax: vscode.DocumentFilter[] = [];
for (const language of this.description.modeIds) {
syntax.push({ language });
for (const scheme of fileSchemes.semanticSupportedSchemes) {
semantic.push({ language, scheme });
}
}
return { semantic, syntax };
}
private async registerProviders(): Promise<void> {
const selector = this.documentSelector;
const cachedResponse = new CachedResponse();
await Promise.all([
import('./languageFeatures/completions').then(provider => this._register(provider.register(selector, this.description.id, this.client, this.typingsStatus, this.fileConfigurationManager, this.commandManager, this.telemetryReporter, this.onCompletionAccepted))),
import('./languageFeatures/definitions').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/directiveCommentCompletions').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/documentHighlight').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/documentSymbol').then(provider => this._register(provider.register(selector, this.client, cachedResponse))),
import('./languageFeatures/folding').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/formatting').then(provider => this._register(provider.register(selector, this.description.id, this.client, this.fileConfigurationManager))),
import('./languageFeatures/hover').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/implementations').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/codeLens/implementationsCodeLens').then(provider => this._register(provider.register(selector, this.description.id, this.client, cachedResponse))),
import('./languageFeatures/jsDocCompletions').then(provider => this._register(provider.register(selector, this.description.id, this.client))),
import('./languageFeatures/organizeImports').then(provider => this._register(provider.register(selector, this.client, this.commandManager, this.fileConfigurationManager, this.telemetryReporter))),
import('./languageFeatures/quickFix').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.client.diagnosticsManager, this.telemetryReporter))),
import('./languageFeatures/fixAll').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager, this.client.diagnosticsManager))),
import('./languageFeatures/refactor').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.telemetryReporter))),
import('./languageFeatures/references').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/codeLens/referencesCodeLens').then(provider => this._register(provider.register(selector, this.description.id, this.client, cachedResponse))),
import('./languageFeatures/rename').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager))),
import('./languageFeatures/smartSelect').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/signatureHelp').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/tagClosing').then(provider => this._register(provider.register(selector, this.description.id, this.client))),
import('./languageFeatures/typeDefinitions').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/semanticTokens').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/callHierarchy').then(provider => this._register(provider.register(selector, this.client))),
]);
}
private configurationChanged(): void {
const config = vscode.workspace.getConfiguration(this.id, null);
this.updateValidate(config.get(validateSetting, true));
this.updateSuggestionDiagnostics(config.get(suggestionSetting, true));
}
public handles(resource: vscode.Uri, doc: vscode.TextDocument): boolean {
if (doc && this.description.modeIds.indexOf(doc.languageId) >= 0) {
return true;
}
const base = basename(resource.fsPath);
return !!base && (!!this.description.configFilePattern && this.description.configFilePattern.test(base));
}
private get id(): string {
return this.description.id;
}
public get diagnosticSource(): string {
return this.description.diagnosticSource;
}
private updateValidate(value: boolean) {
this.client.diagnosticsManager.setValidate(this._diagnosticLanguage, value);
}
private updateSuggestionDiagnostics(value: boolean) {
this.client.diagnosticsManager.setEnableSuggestions(this._diagnosticLanguage, value);
}
public reInitialize(): void {
this.client.diagnosticsManager.reInitialize();
}
public triggerAllDiagnostics(): void {
this.client.bufferSyncSupport.requestAllDiagnostics();
}
public diagnosticsReceived(diagnosticsKind: DiagnosticKind, file: vscode.Uri, diagnostics: (vscode.Diagnostic & { reportUnnecessary: any, reportDeprecated: any })[]): void {
const config = vscode.workspace.getConfiguration(this.id, file);
const reportUnnecessary = config.get<boolean>('showUnused', true);
const reportDeprecated = config.get<boolean>('showDeprecated', true);
this.client.diagnosticsManager.updateDiagnostics(file, this._diagnosticLanguage, diagnosticsKind, diagnostics.filter(diag => {
// Don't both reporting diagnostics we know will not be rendered
if (!reportUnnecessary) {
if (diag.reportUnnecessary && diag.severity === vscode.DiagnosticSeverity.Hint) {
return false;
}
}
if (!reportDeprecated) {
if (diag.reportDeprecated && diag.severity === vscode.DiagnosticSeverity.Hint) {
return false;
}
}
return true;
}));
}
public configFileDiagnosticsReceived(file: vscode.Uri, diagnostics: vscode.Diagnostic[]): void {
this.client.diagnosticsManager.configFileDiagnosticsReceived(file, diagnostics);
}
private get _diagnosticLanguage() {
return this.description.diagnosticLanguage;
}
}

View File

@@ -0,0 +1,91 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { CommandManager } from './commands/commandManager';
import { OngoingRequestCancellerFactory } from './tsServer/cancellation';
import { ILogDirectoryProvider } from './tsServer/logDirectoryProvider';
import { TsServerProcessFactory } from './tsServer/server';
import { ITypeScriptVersionProvider } from './tsServer/versionProvider';
import TypeScriptServiceClientHost from './typeScriptServiceClientHost';
import { flatten } from './utils/arrays';
import * as fileSchemes from './utils/fileSchemes';
import { standardLanguageDescriptions } from './utils/languageDescription';
import { lazy, Lazy } from './utils/lazy';
import ManagedFileContextManager from './utils/managedFileContext';
import { PluginManager } from './utils/plugins';
export function createLazyClientHost(
context: vscode.ExtensionContext,
onCaseInsenitiveFileSystem: boolean,
services: {
pluginManager: PluginManager,
commandManager: CommandManager,
logDirectoryProvider: ILogDirectoryProvider,
cancellerFactory: OngoingRequestCancellerFactory,
versionProvider: ITypeScriptVersionProvider,
processFactory: TsServerProcessFactory,
},
onCompletionAccepted: (item: vscode.CompletionItem) => void,
): Lazy<TypeScriptServiceClientHost> {
return lazy(() => {
const clientHost = new TypeScriptServiceClientHost(
standardLanguageDescriptions,
context.workspaceState,
onCaseInsenitiveFileSystem,
services,
onCompletionAccepted);
context.subscriptions.push(clientHost);
return clientHost;
});
}
export function lazilyActivateClient(
lazyClientHost: Lazy<TypeScriptServiceClientHost>,
pluginManager: PluginManager,
): vscode.Disposable {
const disposables: vscode.Disposable[] = [];
const supportedLanguage = flatten([
...standardLanguageDescriptions.map(x => x.modeIds),
...pluginManager.plugins.map(x => x.languages)
]);
let hasActivated = false;
const maybeActivate = (textDocument: vscode.TextDocument): boolean => {
if (!hasActivated && isSupportedDocument(supportedLanguage, textDocument)) {
hasActivated = true;
// Force activation
void lazyClientHost.value;
disposables.push(new ManagedFileContextManager(resource => {
return lazyClientHost.value.serviceClient.toPath(resource);
}));
return true;
}
return false;
};
const didActivate = vscode.workspace.textDocuments.some(maybeActivate);
if (!didActivate) {
const openListener = vscode.workspace.onDidOpenTextDocument(doc => {
if (maybeActivate(doc)) {
openListener.dispose();
}
}, undefined, disposables);
}
return vscode.Disposable.from(...disposables);
}
function isSupportedDocument(
supportedLanguage: readonly string[],
document: vscode.TextDocument
): boolean {
return supportedLanguage.indexOf(document.languageId) >= 0
&& !fileSchemes.disabledSchemes.has(document.uri.scheme);
}

View File

@@ -0,0 +1,91 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export class Kind {
public static readonly alias = 'alias';
public static readonly callSignature = 'call';
public static readonly class = 'class';
public static readonly const = 'const';
public static readonly constructorImplementation = 'constructor';
public static readonly constructSignature = 'construct';
public static readonly directory = 'directory';
public static readonly enum = 'enum';
public static readonly enumMember = 'enum member';
public static readonly externalModuleName = 'external module name';
public static readonly function = 'function';
public static readonly indexSignature = 'index';
public static readonly interface = 'interface';
public static readonly keyword = 'keyword';
public static readonly let = 'let';
public static readonly localFunction = 'local function';
public static readonly localVariable = 'local var';
public static readonly method = 'method';
public static readonly memberGetAccessor = 'getter';
public static readonly memberSetAccessor = 'setter';
public static readonly memberVariable = 'property';
public static readonly module = 'module';
public static readonly primitiveType = 'primitive type';
public static readonly script = 'script';
public static readonly type = 'type';
public static readonly variable = 'var';
public static readonly warning = 'warning';
public static readonly string = 'string';
public static readonly parameter = 'parameter';
public static readonly typeParameter = 'type parameter';
}
export class DiagnosticCategory {
public static readonly error = 'error';
public static readonly warning = 'warning';
public static readonly suggestion = 'suggestion';
}
export class KindModifiers {
public static readonly optional = 'optional';
public static readonly depreacted = 'deprecated';
public static readonly color = 'color';
public static readonly dtsFile = '.d.ts';
public static readonly tsFile = '.ts';
public static readonly tsxFile = '.tsx';
public static readonly jsFile = '.js';
public static readonly jsxFile = '.jsx';
public static readonly jsonFile = '.json';
public static readonly fileExtensionKindModifiers = [
KindModifiers.dtsFile,
KindModifiers.tsFile,
KindModifiers.tsxFile,
KindModifiers.jsFile,
KindModifiers.jsxFile,
KindModifiers.jsonFile,
];
}
export class DisplayPartKind {
public static readonly functionName = 'functionName';
public static readonly methodName = 'methodName';
public static readonly parameterName = 'parameterName';
public static readonly propertyName = 'propertyName';
public static readonly punctuation = 'punctuation';
public static readonly text = 'text';
}
export enum EventName {
syntaxDiag = 'syntaxDiag',
semanticDiag = 'semanticDiag',
suggestionDiag = 'suggestionDiag',
configFileDiag = 'configFileDiag',
telemetry = 'telemetry',
projectLanguageServiceState = 'projectLanguageServiceState',
projectsUpdatedInBackground = 'projectsUpdatedInBackground',
beginInstallTypes = 'beginInstallTypes',
endInstallTypes = 'endInstallTypes',
typesInstallerInitializationFailed = 'typesInstallerInitializationFailed',
surveyReady = 'surveyReady',
projectLoadingStart = 'projectLoadingStart',
projectLoadingFinish = 'projectLoadingFinish',
}

View File

@@ -0,0 +1,12 @@
import * as Proto from 'typescript/lib/protocol';
export = Proto;
declare enum ServerType {
Syntax = 'syntax',
Semantic = 'semantic',
}
declare module 'typescript/lib/protocol' {
interface Response {
readonly _serverType?: ServerType;
}
}

View File

@@ -0,0 +1,303 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as jsonc from 'jsonc-parser';
import * as path from 'path';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { wait } from '../test/testUtils';
import { ITypeScriptServiceClient, ServerResponse } from '../typescriptService';
import { coalesce, flatten } from '../utils/arrays';
import { Disposable } from '../utils/dispose';
import { exists } from '../utils/fs';
import { isTsConfigFileName } from '../utils/languageDescription';
import { Lazy } from '../utils/lazy';
import { isImplicitProjectConfigFile } from '../utils/tsconfig';
import { TSConfig, TsConfigProvider } from './tsconfigProvider';
const localize = nls.loadMessageBundle();
enum AutoDetect {
on = 'on',
off = 'off',
build = 'build',
watch = 'watch'
}
interface TypeScriptTaskDefinition extends vscode.TaskDefinition {
tsconfig: string;
option?: string;
}
/**
* Provides tasks for building `tsconfig.json` files in a project.
*/
class TscTaskProvider extends Disposable implements vscode.TaskProvider {
private readonly projectInfoRequestTimeout = 2000;
private readonly findConfigFilesTimeout = 5000;
private autoDetect = AutoDetect.on;
private readonly tsconfigProvider: TsConfigProvider;
public constructor(
private readonly client: Lazy<ITypeScriptServiceClient>
) {
super();
this.tsconfigProvider = new TsConfigProvider();
this._register(vscode.workspace.onDidChangeConfiguration(this.onConfigurationChanged, this));
this.onConfigurationChanged();
}
public async provideTasks(token: vscode.CancellationToken): Promise<vscode.Task[]> {
const folders = vscode.workspace.workspaceFolders;
if ((this.autoDetect === AutoDetect.off) || !folders || !folders.length) {
return [];
}
const configPaths: Set<string> = new Set();
const tasks: vscode.Task[] = [];
for (const project of await this.getAllTsConfigs(token)) {
if (!configPaths.has(project.fsPath)) {
configPaths.add(project.fsPath);
tasks.push(...(await this.getTasksForProject(project)));
}
}
return tasks;
}
public async resolveTask(task: vscode.Task): Promise<vscode.Task | undefined> {
const definition = <TypeScriptTaskDefinition>task.definition;
if (/\\tsconfig.*\.json/.test(definition.tsconfig)) {
// Warn that the task has the wrong slash type
vscode.window.showWarningMessage(localize('badTsConfig', "TypeScript Task in tasks.json contains \"\\\\\". TypeScript tasks tsconfig must use \"/\""));
return undefined;
}
const tsconfigPath = definition.tsconfig;
if (!tsconfigPath) {
return undefined;
}
if (task.scope === undefined || task.scope === vscode.TaskScope.Global || task.scope === vscode.TaskScope.Workspace) {
// scope is required to be a WorkspaceFolder for resolveTask
return undefined;
}
const tsconfigUri = task.scope.uri.with({ path: task.scope.uri.path + '/' + tsconfigPath });
const tsconfig: TSConfig = {
uri: tsconfigUri,
fsPath: tsconfigUri.fsPath,
posixPath: tsconfigUri.path,
workspaceFolder: task.scope
};
return this.getTasksForProjectAndDefinition(tsconfig, definition);
}
private async getAllTsConfigs(token: vscode.CancellationToken): Promise<TSConfig[]> {
const configs = flatten(await Promise.all([
this.getTsConfigForActiveFile(token),
this.getTsConfigsInWorkspace(token),
]));
return Promise.all(
configs.map(async config => await exists(config.uri) ? config : undefined),
).then(coalesce);
}
private async getTsConfigForActiveFile(token: vscode.CancellationToken): Promise<TSConfig[]> {
const editor = vscode.window.activeTextEditor;
if (editor) {
if (isTsConfigFileName(editor.document.fileName)) {
const uri = editor.document.uri;
return [{
uri,
fsPath: uri.fsPath,
posixPath: uri.path,
workspaceFolder: vscode.workspace.getWorkspaceFolder(uri)
}];
}
}
const file = this.getActiveTypeScriptFile();
if (!file) {
return [];
}
const response = await Promise.race([
this.client.value.execute(
'projectInfo',
{ file, needFileNameList: false },
token),
new Promise<typeof ServerResponse.NoContent>(resolve => setTimeout(() => resolve(ServerResponse.NoContent), this.projectInfoRequestTimeout))
]);
if (response.type !== 'response' || !response.body) {
return [];
}
const { configFileName } = response.body;
if (configFileName && !isImplicitProjectConfigFile(configFileName)) {
const normalizedConfigPath = path.normalize(configFileName);
const uri = vscode.Uri.file(normalizedConfigPath);
const folder = vscode.workspace.getWorkspaceFolder(uri);
return [{
uri,
fsPath: normalizedConfigPath,
posixPath: uri.path,
workspaceFolder: folder
}];
}
return [];
}
private async getTsConfigsInWorkspace(token: vscode.CancellationToken): Promise<TSConfig[]> {
const getConfigsTimeout = new vscode.CancellationTokenSource();
token.onCancellationRequested(() => getConfigsTimeout.cancel());
return Promise.race([
this.tsconfigProvider.getConfigsForWorkspace(getConfigsTimeout.token).then(x => Array.from(x)),
wait(this.findConfigFilesTimeout).then(() => {
getConfigsTimeout.cancel();
return [];
}),
]);
}
private static async getCommand(project: TSConfig): Promise<string> {
if (project.workspaceFolder) {
const localTsc = await TscTaskProvider.getLocalTscAtPath(path.dirname(project.fsPath));
if (localTsc) {
return localTsc;
}
const workspaceTsc = await TscTaskProvider.getLocalTscAtPath(project.workspaceFolder.uri.fsPath);
if (workspaceTsc) {
return workspaceTsc;
}
}
// Use global tsc version
return 'tsc';
}
private static async getLocalTscAtPath(folderPath: string): Promise<string | undefined> {
const platform = process.platform;
const bin = path.join(folderPath, 'node_modules', '.bin');
if (platform === 'win32' && await exists(vscode.Uri.file(path.join(bin, 'tsc.cmd')))) {
return path.join(bin, 'tsc.cmd');
} else if ((platform === 'linux' || platform === 'darwin') && await exists(vscode.Uri.file(path.join(bin, 'tsc')))) {
return path.join(bin, 'tsc');
}
return undefined;
}
private getActiveTypeScriptFile(): string | undefined {
const editor = vscode.window.activeTextEditor;
if (editor) {
const document = editor.document;
if (document && (document.languageId === 'typescript' || document.languageId === 'typescriptreact')) {
return this.client.value.toPath(document.uri);
}
}
return undefined;
}
private getBuildTask(workspaceFolder: vscode.WorkspaceFolder | undefined, label: string, command: string, args: string[], buildTaskidentifier: TypeScriptTaskDefinition): vscode.Task {
const buildTask = new vscode.Task(
buildTaskidentifier,
workspaceFolder || vscode.TaskScope.Workspace,
localize('buildTscLabel', 'build - {0}', label),
'tsc',
new vscode.ShellExecution(command, args),
'$tsc');
buildTask.group = vscode.TaskGroup.Build;
buildTask.isBackground = false;
return buildTask;
}
private getWatchTask(workspaceFolder: vscode.WorkspaceFolder | undefined, label: string, command: string, args: string[], watchTaskidentifier: TypeScriptTaskDefinition) {
const watchTask = new vscode.Task(
watchTaskidentifier,
workspaceFolder || vscode.TaskScope.Workspace,
localize('buildAndWatchTscLabel', 'watch - {0}', label),
'tsc',
new vscode.ShellExecution(command, [...args, '--watch']),
'$tsc-watch');
watchTask.group = vscode.TaskGroup.Build;
watchTask.isBackground = true;
return watchTask;
}
private async getTasksForProject(project: TSConfig): Promise<vscode.Task[]> {
const command = await TscTaskProvider.getCommand(project);
const args = await this.getBuildShellArgs(project);
const label = this.getLabelForTasks(project);
const tasks: vscode.Task[] = [];
if (this.autoDetect === AutoDetect.build || this.autoDetect === AutoDetect.on) {
tasks.push(this.getBuildTask(project.workspaceFolder, label, command, args, { type: 'typescript', tsconfig: label }));
}
if (this.autoDetect === AutoDetect.watch || this.autoDetect === AutoDetect.on) {
tasks.push(this.getWatchTask(project.workspaceFolder, label, command, args, { type: 'typescript', tsconfig: label, option: 'watch' }));
}
return tasks;
}
private async getTasksForProjectAndDefinition(project: TSConfig, definition: TypeScriptTaskDefinition): Promise<vscode.Task | undefined> {
const command = await TscTaskProvider.getCommand(project);
const args = await this.getBuildShellArgs(project);
const label = this.getLabelForTasks(project);
let task: vscode.Task | undefined;
if (definition.option === undefined) {
task = this.getBuildTask(project.workspaceFolder, label, command, args, definition);
} else if (definition.option === 'watch') {
task = this.getWatchTask(project.workspaceFolder, label, command, args, definition);
}
return task;
}
private async getBuildShellArgs(project: TSConfig): Promise<Array<string>> {
const defaultArgs = ['-p', project.fsPath];
try {
const bytes = await vscode.workspace.fs.readFile(project.uri);
const text = Buffer.from(bytes).toString('utf-8');
const tsconfig = jsonc.parse(text);
if (tsconfig?.references) {
return ['-b', project.fsPath];
}
} catch {
// noops
}
return defaultArgs;
}
private getLabelForTasks(project: TSConfig): string {
if (project.workspaceFolder) {
const workspaceNormalizedUri = vscode.Uri.file(path.normalize(project.workspaceFolder.uri.fsPath)); // Make sure the drive letter is lowercase
return path.posix.relative(workspaceNormalizedUri.path, project.posixPath);
}
return project.posixPath;
}
private onConfigurationChanged(): void {
const type = vscode.workspace.getConfiguration('typescript.tsc').get<AutoDetect>('autoDetect');
this.autoDetect = typeof type === 'undefined' ? AutoDetect.on : type;
}
}
export function register(
lazyClient: Lazy<ITypeScriptServiceClient>,
) {
return vscode.tasks.registerTaskProvider('typescript', new TscTaskProvider(lazyClient));
}

View File

@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
export interface TSConfig {
readonly uri: vscode.Uri;
readonly fsPath: string;
readonly posixPath: string;
readonly workspaceFolder?: vscode.WorkspaceFolder;
}
export class TsConfigProvider {
public async getConfigsForWorkspace(token: vscode.CancellationToken): Promise<Iterable<TSConfig>> {
if (!vscode.workspace.workspaceFolders) {
return [];
}
const configs = new Map<string, TSConfig>();
for (const config of await this.findConfigFiles(token)) {
const root = vscode.workspace.getWorkspaceFolder(config);
if (root) {
configs.set(config.fsPath, {
uri: config,
fsPath: config.fsPath,
posixPath: config.path,
workspaceFolder: root
});
}
}
return configs.values();
}
private async findConfigFiles(token: vscode.CancellationToken): Promise<vscode.Uri[]> {
return await vscode.workspace.findFiles('**/tsconfig*.json', '**/{node_modules,.*}/**', undefined, token);
}
}

View File

@@ -0,0 +1,125 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { CachedResponse } from '../tsServer/cachedResponse';
import { ServerResponse } from '../typescriptService';
suite('CachedResponse', () => {
test('should cache simple response for same document', async () => {
const doc = await createTextDocument();
const response = new CachedResponse();
assertResult(await response.execute(doc, respondWith('test-0')), 'test-0');
assertResult(await response.execute(doc, respondWith('test-1')), 'test-0');
});
test('should invalidate cache for new document', async () => {
const doc1 = await createTextDocument();
const doc2 = await createTextDocument();
const response = new CachedResponse();
assertResult(await response.execute(doc1, respondWith('test-0')), 'test-0');
assertResult(await response.execute(doc1, respondWith('test-1')), 'test-0');
assertResult(await response.execute(doc2, respondWith('test-2')), 'test-2');
assertResult(await response.execute(doc2, respondWith('test-3')), 'test-2');
assertResult(await response.execute(doc1, respondWith('test-4')), 'test-4');
assertResult(await response.execute(doc1, respondWith('test-5')), 'test-4');
});
test('should not cache cancelled responses', async () => {
const doc = await createTextDocument();
const response = new CachedResponse();
const cancelledResponder = createEventualResponder<ServerResponse.Cancelled>();
const result1 = response.execute(doc, () => cancelledResponder.promise);
const result2 = response.execute(doc, respondWith('test-0'));
const result3 = response.execute(doc, respondWith('test-1'));
cancelledResponder.resolve(new ServerResponse.Cancelled('cancelled'));
assert.strictEqual((await result1).type, 'cancelled');
assertResult(await result2, 'test-0');
assertResult(await result3, 'test-0');
});
test('should not care if subsequent requests are cancelled if first request is resolved ok', async () => {
const doc = await createTextDocument();
const response = new CachedResponse();
const cancelledResponder = createEventualResponder<ServerResponse.Cancelled>();
const result1 = response.execute(doc, respondWith('test-0'));
const result2 = response.execute(doc, () => cancelledResponder.promise);
const result3 = response.execute(doc, respondWith('test-1'));
cancelledResponder.resolve(new ServerResponse.Cancelled('cancelled'));
assertResult(await result1, 'test-0');
assertResult(await result2, 'test-0');
assertResult(await result3, 'test-0');
});
test('should not cache cancelled responses with document changes', async () => {
const doc1 = await createTextDocument();
const doc2 = await createTextDocument();
const response = new CachedResponse();
const cancelledResponder = createEventualResponder<ServerResponse.Cancelled>();
const cancelledResponder2 = createEventualResponder<ServerResponse.Cancelled>();
const result1 = response.execute(doc1, () => cancelledResponder.promise);
const result2 = response.execute(doc1, respondWith('test-0'));
const result3 = response.execute(doc1, respondWith('test-1'));
const result4 = response.execute(doc2, () => cancelledResponder2.promise);
const result5 = response.execute(doc2, respondWith('test-2'));
const result6 = response.execute(doc1, respondWith('test-3'));
cancelledResponder.resolve(new ServerResponse.Cancelled('cancelled'));
cancelledResponder2.resolve(new ServerResponse.Cancelled('cancelled'));
assert.strictEqual((await result1).type, 'cancelled');
assertResult(await result2, 'test-0');
assertResult(await result3, 'test-0');
assert.strictEqual((await result4).type, 'cancelled');
assertResult(await result5, 'test-2');
assertResult(await result6, 'test-3');
});
});
function respondWith(command: string) {
return async () => createResponse(command);
}
function createTextDocument() {
return vscode.workspace.openTextDocument({ language: 'javascript', content: '' });
}
function assertResult(result: ServerResponse.Response<Proto.Response>, command: string) {
if (result.type === 'response') {
assert.strictEqual(result.command, command);
} else {
assert.fail('Response failed');
}
}
function createResponse(command: string): Proto.Response {
return {
type: 'response',
body: {},
command: command,
request_seq: 1,
success: true,
seq: 1
};
}
function createEventualResponder<T>(): { promise: Promise<T>, resolve: (x: T) => void } {
let resolve: (value: T) => void;
const promise = new Promise<T>(r => { resolve = r; });
return { promise, resolve: resolve! };
}

View File

@@ -0,0 +1,652 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as vscode from 'vscode';
import { disposeAll } from '../utils/dispose';
import { acceptFirstSuggestion, typeCommitCharacter } from './suggestTestHelpers';
import { assertEditorContents, Config, createTestEditor, joinLines, updateConfig, VsCodeConfiguration, wait, enumerateConfig } from './testUtils';
const testDocumentUri = vscode.Uri.parse('untitled:test.ts');
const insertModes = Object.freeze(['insert', 'replace']);
suite('TypeScript Completions', () => {
const configDefaults: VsCodeConfiguration = Object.freeze({
[Config.autoClosingBrackets]: 'always',
[Config.typescriptCompleteFunctionCalls]: false,
[Config.insertMode]: 'insert',
[Config.snippetSuggestions]: 'none',
[Config.suggestSelection]: 'first',
[Config.javascriptQuoteStyle]: 'double',
[Config.typescriptQuoteStyle]: 'double',
});
const _disposables: vscode.Disposable[] = [];
let oldConfig: { [key: string]: any } = {};
setup(async () => {
await wait(500);
// Save off config and apply defaults
oldConfig = await updateConfig(testDocumentUri, configDefaults);
});
teardown(async () => {
disposeAll(_disposables);
// Restore config
await updateConfig(testDocumentUri, oldConfig);
return vscode.commands.executeCommand('workbench.action.closeAllEditors');
});
test('Basic var completion', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`const abcdef = 123;`,
`ab$0;`
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`const abcdef = 123;`,
`abcdef;`
),
`config: ${config}`
);
});
});
test('Should treat period as commit character for var completions', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`const abcdef = 123;`,
`ab$0;`
);
await typeCommitCharacter(testDocumentUri, '.', _disposables);
assertEditorContents(editor,
joinLines(
`const abcdef = 123;`,
`abcdef.;`
),
`config: ${config}`);
});
});
test('Should treat paren as commit character for function completions', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`function abcdef() {};`,
`ab$0;`
);
await typeCommitCharacter(testDocumentUri, '(', _disposables);
assertEditorContents(editor,
joinLines(
`function abcdef() {};`,
`abcdef();`
), `config: ${config}`);
});
});
test('Should insert backets when completing dot properties with spaces in name', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
'const x = { "hello world": 1 };',
'x.$0'
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
'const x = { "hello world": 1 };',
'x["hello world"]'
), `config: ${config}`);
});
});
test('Should allow commit characters for backet completions', async () => {
for (const { char, insert } of [
{ char: '.', insert: '.' },
{ char: '(', insert: '()' },
]) {
const editor = await createTestEditor(testDocumentUri,
'const x = { "hello world2": 1 };',
'x.$0'
);
await typeCommitCharacter(testDocumentUri, char, _disposables);
assertEditorContents(editor,
joinLines(
'const x = { "hello world2": 1 };',
`x["hello world2"]${insert}`
));
disposeAll(_disposables);
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
}
});
test('Should not prioritize bracket accessor completions. #63100', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
// 'a' should be first entry in completion list
const editor = await createTestEditor(testDocumentUri,
'const x = { "z-z": 1, a: 1 };',
'x.$0'
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
'const x = { "z-z": 1, a: 1 };',
'x.a'
),
`config: ${config}`);
});
});
test('Accepting a string completion should replace the entire string. #53962', async () => {
const editor = await createTestEditor(testDocumentUri,
'interface TFunction {',
` (_: 'abc.abc2', __ ?: {}): string;`,
` (_: 'abc.abc', __?: {}): string;`,
`}`,
'const f: TFunction = (() => { }) as any;',
`f('abc.abc$0')`
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
'interface TFunction {',
` (_: 'abc.abc2', __ ?: {}): string;`,
` (_: 'abc.abc', __?: {}): string;`,
`}`,
'const f: TFunction = (() => { }) as any;',
`f('abc.abc')`
));
});
test('completeFunctionCalls should complete function parameters when at end of word', async () => {
await updateConfig(testDocumentUri, { [Config.typescriptCompleteFunctionCalls]: true });
// Complete with-in word
const editor = await createTestEditor(testDocumentUri,
`function abcdef(x, y, z) { }`,
`abcdef$0`
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`function abcdef(x, y, z) { }`,
`abcdef(x, y, z)`
));
});
test.skip('completeFunctionCalls should complete function parameters when within word', async () => {
await updateConfig(testDocumentUri, { [Config.typescriptCompleteFunctionCalls]: true });
const editor = await createTestEditor(testDocumentUri,
`function abcdef(x, y, z) { }`,
`abcd$0ef`
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`function abcdef(x, y, z) { }`,
`abcdef(x, y, z)`
));
});
test('completeFunctionCalls should not complete function parameters at end of word if we are already in something that looks like a function call, #18131', async () => {
await updateConfig(testDocumentUri, { [Config.typescriptCompleteFunctionCalls]: true });
const editor = await createTestEditor(testDocumentUri,
`function abcdef(x, y, z) { }`,
`abcdef$0(1, 2, 3)`
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`function abcdef(x, y, z) { }`,
`abcdef(1, 2, 3)`
));
});
test.skip('completeFunctionCalls should not complete function parameters within word if we are already in something that looks like a function call, #18131', async () => {
await updateConfig(testDocumentUri, { [Config.typescriptCompleteFunctionCalls]: true });
const editor = await createTestEditor(testDocumentUri,
`function abcdef(x, y, z) { }`,
`abcd$0ef(1, 2, 3)`
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`function abcdef(x, y, z) { }`,
`abcdef(1, 2, 3)`
));
});
test('should not de-prioritize `this.member` suggestion, #74164', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` private detail = '';`,
` foo() {`,
` det$0`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` private detail = '';`,
` foo() {`,
` this.detail`,
` }`,
`}`,
),
`Config: ${config}`);
});
});
test('Member completions for string property name should insert `this.` and use brackets', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` ['xyz 123'] = 1`,
` foo() {`,
` xyz$0`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` ['xyz 123'] = 1`,
` foo() {`,
` this["xyz 123"]`,
` }`,
`}`,
),
`Config: ${config}`);
});
});
test('Member completions for string property name already using `this.` should add brackets', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` ['xyz 123'] = 1`,
` foo() {`,
` this.xyz$0`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` ['xyz 123'] = 1`,
` foo() {`,
` this["xyz 123"]`,
` }`,
`}`,
),
`Config: ${config}`);
});
});
test('Accepting a completion in word using `insert` mode should insert', async () => {
await updateConfig(testDocumentUri, { [Config.insertMode]: 'insert' });
const editor = await createTestEditor(testDocumentUri,
`const abc = 123;`,
`ab$0c`
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`const abc = 123;`,
`abcc`
));
});
test('Accepting a completion in word using `replace` mode should replace', async () => {
await updateConfig(testDocumentUri, { [Config.insertMode]: 'replace' });
const editor = await createTestEditor(testDocumentUri,
`const abc = 123;`,
`ab$0c`
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`const abc = 123;`,
`abc`
));
});
test('Accepting a member completion in word using `insert` mode add `this.` and insert', async () => {
await updateConfig(testDocumentUri, { [Config.insertMode]: 'insert' });
const editor = await createTestEditor(testDocumentUri,
`class Foo {`,
` abc = 1;`,
` foo() {`,
` ab$0c`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class Foo {`,
` abc = 1;`,
` foo() {`,
` this.abcc`,
` }`,
`}`,
));
});
test('Accepting a member completion in word using `replace` mode should add `this.` and replace', async () => {
await updateConfig(testDocumentUri, { [Config.insertMode]: 'replace' });
const editor = await createTestEditor(testDocumentUri,
`class Foo {`,
` abc = 1;`,
` foo() {`,
` ab$0c`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class Foo {`,
` abc = 1;`,
` foo() {`,
` this.abc`,
` }`,
`}`,
));
});
test('Accepting string completion inside string using `insert` mode should insert', async () => {
await updateConfig(testDocumentUri, { [Config.insertMode]: 'insert' });
const editor = await createTestEditor(testDocumentUri,
`const abc = { 'xy z': 123 }`,
`abc["x$0y w"]`
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`const abc = { 'xy z': 123 }`,
`abc["xy zy w"]`
));
});
// Waiting on https://github.com/microsoft/TypeScript/issues/35602
test.skip('Accepting string completion inside string using insert mode should insert', async () => {
await updateConfig(testDocumentUri, { [Config.insertMode]: 'replace' });
const editor = await createTestEditor(testDocumentUri,
`const abc = { 'xy z': 123 }`,
`abc["x$0y w"]`
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`const abc = { 'xy z': 123 }`,
`abc["xy w"]`
));
});
test('Private field completions on `this.#` should work', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` #xyz = 1;`,
` foo() {`,
` this.#$0`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` #xyz = 1;`,
` foo() {`,
` this.#xyz`,
` }`,
`}`,
),
`Config: ${config}`);
});
});
test('Private field completions on `#` should insert `this.`', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` #xyz = 1;`,
` foo() {`,
` #$0`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` #xyz = 1;`,
` foo() {`,
` this.#xyz`,
` }`,
`}`,
),
`Config: ${config}`);
});
});
test('Private field completions should not require strict prefix match (#89556)', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` #xyz = 1;`,
` foo() {`,
` this.xyz$0`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` #xyz = 1;`,
` foo() {`,
` this.#xyz`,
` }`,
`}`,
),
`Config: ${config}`);
});
});
test('Private field completions without `this.` should not require strict prefix match (#89556)', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` #xyz = 1;`,
` foo() {`,
` xyz$0`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` #xyz = 1;`,
` foo() {`,
` this.#xyz`,
` }`,
`}`,
),
`Config: ${config}`);
});
});
test('Accepting a completion for async property in `insert` mode should insert and add await', async () => {
await updateConfig(testDocumentUri, { [Config.insertMode]: 'insert' });
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` xyz = Promise.resolve({ 'abc': 1 });`,
` async foo() {`,
` this.xyz.ab$0c`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` xyz = Promise.resolve({ 'abc': 1 });`,
` async foo() {`,
` (await this.xyz).abcc`,
` }`,
`}`,
));
});
test('Accepting a completion for async property in `replace` mode should replace and add await', async () => {
await updateConfig(testDocumentUri, { [Config.insertMode]: 'replace' });
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` xyz = Promise.resolve({ 'abc': 1 });`,
` async foo() {`,
` this.xyz.ab$0c`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` xyz = Promise.resolve({ 'abc': 1 });`,
` async foo() {`,
` (await this.xyz).abc`,
` }`,
`}`,
));
});
test.skip('Accepting a completion for async string property should add await plus brackets', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModes, async config => {
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` xyz = Promise.resolve({ 'ab c': 1 });`,
` async foo() {`,
` this.xyz.ab$0`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` xyz = Promise.resolve({ 'abc': 1 });`,
` async foo() {`,
` (await this.xyz)["ab c"]`,
` }`,
`}`,
),
`Config: ${config}`);
});
});
test('Replace should work after this. (#91105)', async () => {
await updateConfig(testDocumentUri, { [Config.insertMode]: 'replace' });
const editor = await createTestEditor(testDocumentUri,
`class A {`,
` abc = 1`,
` foo() {`,
` this.$0abc`,
` }`,
`}`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`class A {`,
` abc = 1`,
` foo() {`,
` this.abc`,
` }`,
`}`,
));
});
});

View File

@@ -0,0 +1,139 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as assert from 'assert';
import * as vscode from 'vscode';
import { disposeAll } from '../utils/dispose';
import { createTestEditor, wait, joinLines } from './testUtils';
const testDocumentUri = vscode.Uri.parse('untitled:test.ts');
const emptyRange = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0));
suite('TypeScript Fix All', () => {
const _disposables: vscode.Disposable[] = [];
teardown(async () => {
disposeAll(_disposables);
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
});
test('Fix all should remove unreachable code', async () => {
const editor = await createTestEditor(testDocumentUri,
`function foo() {`,
` return 1;`,
` return 2;`,
`};`,
`function boo() {`,
` return 3;`,
` return 4;`,
`};`,
);
await wait(2000);
const fixes = await vscode.commands.executeCommand<vscode.CodeAction[]>('vscode.executeCodeActionProvider',
testDocumentUri,
emptyRange,
vscode.CodeActionKind.SourceFixAll
);
await vscode.workspace.applyEdit(fixes![0].edit!);
assert.strictEqual(editor.document.getText(), joinLines(
`function foo() {`,
` return 1;`,
`};`,
`function boo() {`,
` return 3;`,
`};`,
));
});
test('Fix all should implement interfaces', async () => {
const editor = await createTestEditor(testDocumentUri,
`interface I {`,
` x: number;`,
`}`,
`class A implements I {}`,
`class B implements I {}`,
);
await wait(2000);
const fixes = await vscode.commands.executeCommand<vscode.CodeAction[]>('vscode.executeCodeActionProvider',
testDocumentUri,
emptyRange,
vscode.CodeActionKind.SourceFixAll
);
await vscode.workspace.applyEdit(fixes![0].edit!);
assert.strictEqual(editor.document.getText(), joinLines(
`interface I {`,
` x: number;`,
`}`,
`class A implements I {`,
` x: number;`,
`}`,
`class B implements I {`,
` x: number;`,
`}`,
));
});
test('Remove unused should handle nested ununused', async () => {
const editor = await createTestEditor(testDocumentUri,
`export const _ = 1;`,
`function unused() {`,
` const a = 1;`,
`}`,
`function used() {`,
` const a = 1;`,
`}`,
`used();`
);
await wait(2000);
const fixes = await vscode.commands.executeCommand<vscode.CodeAction[]>('vscode.executeCodeActionProvider',
testDocumentUri,
emptyRange,
vscode.CodeActionKind.Source.append('removeUnused')
);
await vscode.workspace.applyEdit(fixes![0].edit!);
assert.strictEqual(editor.document.getText(), joinLines(
`export const _ = 1;`,
`function used() {`,
`}`,
`used();`
));
});
test('Remove unused should remove unused interfaces', async () => {
const editor = await createTestEditor(testDocumentUri,
`export const _ = 1;`,
`interface Foo {}`
);
await wait(2000);
const fixes = await vscode.commands.executeCommand<vscode.CodeAction[]>('vscode.executeCodeActionProvider',
testDocumentUri,
emptyRange,
vscode.CodeActionKind.Source.append('removeUnused')
);
await vscode.workspace.applyEdit(fixes![0].edit!);
assert.strictEqual(editor.document.getText(), joinLines(
`export const _ = 1;`,
``
));
});
});

View File

@@ -0,0 +1,140 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// todo@matt
/* eslint code-no-unexternalized-strings: 0 */
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { snippetForFunctionCall } from '../utils/snippetForFunctionCall';
suite('typescript function call snippets', () => {
test('Should use label as function name', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'abc', },
[]
).snippet.value,
'abc()$0');
});
test('Should use insertText string to override function name', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'abc', insertText: 'def' },
[]
).snippet.value,
'def()$0');
});
test('Should return insertText as-is if it is already a snippet', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'abc', insertText: new vscode.SnippetString('bla()$0') },
[]
).snippet.value,
'bla()$0');
});
test('Should return insertText as-is if it is already a snippet', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'abc', insertText: new vscode.SnippetString('bla()$0') },
[]
).snippet.value,
'bla()$0');
});
test('Should extract parameter from display parts', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'activate' },
[{ "text": "function", "kind": "keyword" }, { "text": " ", "kind": "space" }, { "text": "activate", "kind": "text" }, { "text": "(", "kind": "punctuation" }, { "text": "context", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "vscode", "kind": "aliasName" }, { "text": ".", "kind": "punctuation" }, { "text": "ExtensionContext", "kind": "interfaceName" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "void", "kind": "keyword" }]
).snippet.value,
'activate(${1:context})$0');
});
test('Should extract all parameters from display parts', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'foo' },
[{ "text": "function", "kind": "keyword" }, { "text": " ", "kind": "space" }, { "text": "foo", "kind": "functionName" }, { "text": "(", "kind": "punctuation" }, { "text": "a", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "string", "kind": "keyword" }, { "text": ",", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "b", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "number", "kind": "keyword" }, { "text": ",", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "c", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "boolean", "kind": "keyword" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "void", "kind": "keyword" }]
).snippet.value,
'foo(${1:a}, ${2:b}, ${3:c})$0');
});
test('Should create empty placeholder at rest parameter', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'foo' },
[{ "text": "function", "kind": "keyword" }, { "text": " ", "kind": "space" }, { "text": "foo", "kind": "functionName" }, { "text": "(", "kind": "punctuation" }, { "text": "a", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "string", "kind": "keyword" }, { "text": ",", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "...", "kind": "punctuation" }, { "text": "rest", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "any", "kind": "keyword" }, { "text": "[", "kind": "punctuation" }, { "text": "]", "kind": "punctuation" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "void", "kind": "keyword" }]
).snippet.value,
'foo(${1:a}$2)$0');
});
test('Should skip over inline function and object types when extracting parameters', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'foo' },
[{ "text": "function", "kind": "keyword" }, { "text": " ", "kind": "space" }, { "text": "foo", "kind": "functionName" }, { "text": "(", "kind": "punctuation" }, { "text": "a", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "(", "kind": "punctuation" }, { "text": "x", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "number", "kind": "keyword" }, { "text": ")", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "=>", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "{", "kind": "punctuation" }, { "text": "\n", "kind": "lineBreak" }, { "text": " ", "kind": "space" }, { "text": "f", "kind": "propertyName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "(", "kind": "punctuation" }, { "text": ")", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "=>", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "void", "kind": "keyword" }, { "text": ";", "kind": "punctuation" }, { "text": "\n", "kind": "lineBreak" }, { "text": "}", "kind": "punctuation" }, { "text": ",", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "b", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "{", "kind": "punctuation" }, { "text": "\n", "kind": "lineBreak" }, { "text": " ", "kind": "space" }, { "text": "f", "kind": "propertyName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "(", "kind": "punctuation" }, { "text": ")", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "=>", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "void", "kind": "keyword" }, { "text": ";", "kind": "punctuation" }, { "text": "\n", "kind": "lineBreak" }, { "text": "}", "kind": "punctuation" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "void", "kind": "keyword" }]
).snippet.value,
'foo(${1:a}, ${2:b})$0');
});
test('Should skip over return type while extracting parameters', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'foo' },
[{ "text": "function", "kind": "keyword" }, { "text": " ", "kind": "space" }, { "text": "foo", "kind": "functionName" }, { "text": "(", "kind": "punctuation" }, { "text": "a", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "number", "kind": "keyword" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "{", "kind": "punctuation" }, { "text": "\n", "kind": "lineBreak" }, { "text": " ", "kind": "space" }, { "text": "f", "kind": "propertyName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "(", "kind": "punctuation" }, { "text": "b", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "number", "kind": "keyword" }, { "text": ")", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "=>", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "number", "kind": "keyword" }, { "text": ";", "kind": "punctuation" }, { "text": "\n", "kind": "lineBreak" }, { "text": "}", "kind": "punctuation" }]
).snippet.value,
'foo(${1:a})$0');
});
test('Should skip over prefix type while extracting parameters', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'foo' },
[{ "text": "(", "kind": "punctuation" }, { "text": "method", "kind": "text" }, { "text": ")", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "Array", "kind": "localName" }, { "text": "<", "kind": "punctuation" }, { "text": "{", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "dispose", "kind": "methodName" }, { "text": "(", "kind": "punctuation" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "any", "kind": "keyword" }, { "text": ";", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "}", "kind": "punctuation" }, { "text": ">", "kind": "punctuation" }, { "text": ".", "kind": "punctuation" }, { "text": "foo", "kind": "methodName" }, { "text": "(", "kind": "punctuation" }, { "text": "searchElement", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "{", "kind": "punctuation" }, { "text": "\n", "kind": "lineBreak" }, { "text": " ", "kind": "space" }, { "text": "dispose", "kind": "methodName" }, { "text": "(", "kind": "punctuation" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "any", "kind": "keyword" }, { "text": ";", "kind": "punctuation" }, { "text": "\n", "kind": "lineBreak" }, { "text": "}", "kind": "punctuation" }, { "text": ",", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "fromIndex", "kind": "parameterName" }, { "text": "?", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "number", "kind": "keyword" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "number", "kind": "keyword" }]
).snippet.value,
'foo(${1:searchElement}$2)$0');
});
test('Should complete property names', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'methoda' },
[{ "text": "(", "kind": "punctuation" }, { "text": "method", "kind": "text" }, { "text": ")", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "methoda", "kind": "propertyName" }, { "text": "(", "kind": "punctuation" }, { "text": "x", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "number", "kind": "keyword" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "void", "kind": "keyword" }]
).snippet.value,
'methoda(${1:x})$0');
});
test('Should escape snippet syntax in method name', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: '$abc', },
[]
).snippet.value,
'\\$abc()$0');
});
test('Should not include object key signature in completion, #66297', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'foobar', },
[{ "text": "function", "kind": "keyword" }, { "text": " ", "kind": "space" }, { "text": "foobar", "kind": "functionName" }, { "text": "(", "kind": "punctuation" }, { "text": "param", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "{", "kind": "punctuation" }, { "text": "\n", "kind": "lineBreak" }, { "text": " ", "kind": "space" }, { "text": "[", "kind": "punctuation" }, { "text": "key", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "string", "kind": "keyword" }, { "text": "]", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "string", "kind": "keyword" }, { "text": ";", "kind": "punctuation" }, { "text": "\n", "kind": "lineBreak" }, { "text": "}", "kind": "punctuation" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "void", "kind": "keyword" }]
).snippet.value,
'foobar(${1:param})$0');
});
test('Should skip over this parameter', async () => {
assert.strictEqual(
snippetForFunctionCall(
{ label: 'foobar', },
[{ "text": "function", "kind": "keyword" }, { "text": " ", "kind": "space" }, { "text": "foobar", "kind": "functionName" }, { "text": "(", "kind": "punctuation" }, { "text": "this", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "string", "kind": "keyword" }, { "text": ",", "kind": "punctuation" }, { "text": "param", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "string", "kind": "keyword" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "void", "kind": "keyword" }]
).snippet.value,
'foobar(${1:param})$0');
});
});

View File

@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//
// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING
//
// This file is providing the test runner to use when running extension tests.
// By default the test runner in use is Mocha based.
//
// You can provide your own test runner if you want to override it by exporting
// a function run(testRoot: string, clb: (error:Error) => void) that the extension
// host can call to run the tests. The test runner is expected to use console.log
// to report the results back to the caller. When the tests are finished, return
// a possible error to the callback or null if none.
const testRunner = require('vscode/lib/testrunner');
// You can directly control Mocha options by uncommenting the following lines
// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info
testRunner.configure({
ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.)
useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), // colored output from test results (only windows cannot handle)
timeout: 60000,
});
export = testRunner;

View File

@@ -0,0 +1,61 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as vscode from 'vscode';
import { disposeAll } from '../utils/dispose';
import { acceptFirstSuggestion } from './suggestTestHelpers';
import { assertEditorContents, Config, createTestEditor, CURSOR, enumerateConfig, insertModesValues, joinLines, updateConfig, VsCodeConfiguration, wait } from './testUtils';
const testDocumentUri = vscode.Uri.parse('untitled:test.ts');
suite('JSDoc Completions', () => {
const _disposables: vscode.Disposable[] = [];
const configDefaults: VsCodeConfiguration = Object.freeze({
[Config.snippetSuggestions]: 'inline',
});
let oldConfig: { [key: string]: any } = {};
setup(async () => {
await wait(100);
// Save off config and apply defaults
oldConfig = await updateConfig(testDocumentUri, configDefaults);
});
teardown(async () => {
disposeAll(_disposables);
// Restore config
await updateConfig(testDocumentUri, oldConfig);
return vscode.commands.executeCommand('workbench.action.closeAllEditors');
});
test('Should complete jsdoc inside single line comment', async () => {
await enumerateConfig(testDocumentUri, Config.insertMode, insertModesValues, async config => {
const editor = await createTestEditor(testDocumentUri,
`/**$0 */`,
`function abcdef(x, y) { }`,
);
await acceptFirstSuggestion(testDocumentUri, _disposables);
assertEditorContents(editor,
joinLines(
`/**`,
` * `,
` * @param x ${CURSOR}`,
` * @param y `,
` */`,
`function abcdef(x, y) { }`,
),
`Config: ${config}`);
});
});
});

View File

@@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import { templateToSnippet } from '../languageFeatures/jsDocCompletions';
const joinLines = (...args: string[]) => args.join('\n');
suite('typescript.jsDocSnippet', () => {
test('Should do nothing for single line input', async () => {
const input = `/** */`;
assert.strictEqual(templateToSnippet(input).value, input);
});
test('Should put cursor inside multiline line input', async () => {
assert.strictEqual(
templateToSnippet(joinLines(
'/**',
' * ',
' */'
)).value,
joinLines(
'/**',
' * $0',
' */'
));
});
test('Should add placeholders after each parameter', async () => {
assert.strictEqual(
templateToSnippet(joinLines(
'/**',
' * @param a',
' * @param b',
' */'
)).value,
joinLines(
'/**',
' * @param a ${1}',
' * @param b ${2}',
' */'
));
});
test('Should add placeholders for types', async () => {
assert.strictEqual(
templateToSnippet(joinLines(
'/**',
' * @param {*} a',
' * @param {*} b',
' */'
)).value,
joinLines(
'/**',
' * @param {${1:*}} a ${2}',
' * @param {${3:*}} b ${4}',
' */'
));
});
test('Should properly escape dollars in parameter names', async () => {
assert.strictEqual(
templateToSnippet(joinLines(
'/**',
' * ',
' * @param $arg',
' */'
)).value,
joinLines(
'/**',
' * $0',
' * @param \\$arg ${1}',
' */'
));
});
});

View File

@@ -0,0 +1,54 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { CURSOR, withRandomFileEditor, wait, joinLines } from './testUtils';
const onDocumentChange = (doc: vscode.TextDocument): Promise<vscode.TextDocument> => {
return new Promise<vscode.TextDocument>(resolve => {
const sub = vscode.workspace.onDidChangeTextDocument(e => {
if (e.document !== doc) {
return;
}
sub.dispose();
resolve(e.document);
});
});
};
const type = async (document: vscode.TextDocument, text: string): Promise<vscode.TextDocument> => {
const onChange = onDocumentChange(document);
await vscode.commands.executeCommand('type', { text });
await onChange;
return document;
};
suite('OnEnter', () => {
test('should indent after if block with braces', () => {
return withRandomFileEditor(`if (true) {${CURSOR}`, 'js', async (_editor, document) => {
await type(document, '\nx');
assert.strictEqual(
document.getText(),
joinLines(
`if (true) {`,
` x`));
});
});
test('should indent within empty object literal', () => {
return withRandomFileEditor(`({${CURSOR}})`, 'js', async (_editor, document) => {
await type(document, '\nx');
await wait(500);
assert.strictEqual(
document.getText(),
joinLines(`({`,
` x`,
`})`));
});
});
});

View File

@@ -0,0 +1,68 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import { tagsMarkdownPreview, markdownDocumentation } from '../utils/previewer';
suite('typescript.previewer', () => {
test('Should ignore hyphens after a param tag', async () => {
assert.strictEqual(
tagsMarkdownPreview([
{
name: 'param',
text: 'a - b'
}
]),
'*@param* `a` — b');
});
test('Should parse url jsdoc @link', async () => {
assert.strictEqual(
markdownDocumentation('x {@link http://www.example.com/foo} y {@link https://api.jquery.com/bind/#bind-eventType-eventData-handler} z', []).value,
'x [http://www.example.com/foo](http://www.example.com/foo) y [https://api.jquery.com/bind/#bind-eventType-eventData-handler](https://api.jquery.com/bind/#bind-eventType-eventData-handler) z');
});
test('Should parse url jsdoc @link with text', async () => {
assert.strictEqual(
markdownDocumentation('x {@link http://www.example.com/foo abc xyz} y {@link http://www.example.com/bar|b a z} z', []).value,
'x [abc xyz](http://www.example.com/foo) y [b a z](http://www.example.com/bar) z');
});
test('Should treat @linkcode jsdocs links as monospace', async () => {
assert.strictEqual(
markdownDocumentation('x {@linkcode http://www.example.com/foo} y {@linkplain http://www.example.com/bar} z', []).value,
'x [`http://www.example.com/foo`](http://www.example.com/foo) y [http://www.example.com/bar](http://www.example.com/bar) z');
});
test('Should parse url jsdoc @link in param tag', async () => {
assert.strictEqual(
tagsMarkdownPreview([
{
name: 'param',
text: 'a x {@link http://www.example.com/foo abc xyz} y {@link http://www.example.com/bar|b a z} z'
}
]),
'*@param* `a` — x [abc xyz](http://www.example.com/foo) y [b a z](http://www.example.com/bar) z');
});
test('Should ignore unclosed jsdocs @link', async () => {
assert.strictEqual(
markdownDocumentation('x {@link http://www.example.com/foo y {@link http://www.example.com/bar bar} z', []).value,
'x {@link http://www.example.com/foo y [bar](http://www.example.com/bar) z');
});
test('Should support non-ascii characters in parameter name (#90108)', async () => {
assert.strictEqual(
tagsMarkdownPreview([
{
name: 'param',
text: 'parámetroConDiacríticos this will not'
}
]),
'*@param* `parámetroConDiacríticos` — this will not');
});
});

View File

@@ -0,0 +1,173 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { disposeAll } from '../utils/dispose';
import { createTestEditor, joinLines, retryUntilDocumentChanges, wait } from './testUtils';
suite('TypeScript Quick Fix', () => {
const _disposables: vscode.Disposable[] = [];
teardown(async () => {
disposeAll(_disposables);
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
});
test('Fix all should not be marked as preferred #97866', async () => {
const testDocumentUri = vscode.Uri.parse('untitled:test.ts');
const editor = await createTestEditor(testDocumentUri,
`export const _ = 1;`,
`const a$0 = 1;`,
`const b = 2;`,
);
await retryUntilDocumentChanges(testDocumentUri, { retries: 10, timeout: 500 }, _disposables, () => {
return vscode.commands.executeCommand('editor.action.autoFix');
});
assert.strictEqual(editor.document.getText(), joinLines(
`export const _ = 1;`,
`const b = 2;`,
));
});
test('Add import should be a preferred fix if there is only one possible import', async () => {
const testDocumentUri = workspaceFile('foo.ts');
await createTestEditor(testDocumentUri,
`export const foo = 1;`);
const editor = await createTestEditor(workspaceFile('index.ts'),
`export const _ = 1;`,
`foo$0;`
);
await retryUntilDocumentChanges(testDocumentUri, { retries: 10, timeout: 500 }, _disposables, () => {
return vscode.commands.executeCommand('editor.action.autoFix');
});
// Document should not have been changed here
assert.strictEqual(editor.document.getText(), joinLines(
`import { foo } from "./foo";`,
``,
`export const _ = 1;`,
`foo;`
));
});
test('Add import should not be a preferred fix if are multiple possible imports', async () => {
await createTestEditor(workspaceFile('foo.ts'),
`export const foo = 1;`);
await createTestEditor(workspaceFile('bar.ts'),
`export const foo = 1;`);
const editor = await createTestEditor(workspaceFile('index.ts'),
`export const _ = 1;`,
`foo$0;`
);
await wait(3000);
await vscode.commands.executeCommand('editor.action.autoFix');
await wait(500);
assert.strictEqual(editor.document.getText(), joinLines(
`export const _ = 1;`,
`foo;`
));
});
test('Only a single ts-ignore should be returned if there are multiple errors on one line #98274', async () => {
const testDocumentUri = workspaceFile('foojs.js');
const editor = await createTestEditor(testDocumentUri,
`//@ts-check`,
`const a = require('./bla');`);
await wait(3000);
const fixes = await vscode.commands.executeCommand<vscode.CodeAction[]>('vscode.executeCodeActionProvider',
testDocumentUri,
editor.document.lineAt(1).range
);
const ignoreFixes = fixes?.filter(x => x.title === 'Ignore this error message');
assert.strictEqual(ignoreFixes?.length, 1);
});
test('Should prioritize implement interface over remove unused #94212', async () => {
const testDocumentUri = workspaceFile('foo.ts');
const editor = await createTestEditor(testDocumentUri,
`export interface IFoo { value: string; }`,
`class Foo implements IFoo { }`);
await wait(3000);
const fixes = await vscode.commands.executeCommand<vscode.CodeAction[]>('vscode.executeCodeActionProvider',
testDocumentUri,
editor.document.lineAt(1).range
);
assert.strictEqual(fixes?.length, 2);
assert.strictEqual(fixes![0].title, `Implement interface 'IFoo'`);
assert.strictEqual(fixes![1].title, `Remove unused declaration for: 'Foo'`);
});
test('Should prioritize implement abstract class over remove unused #101486', async () => {
const testDocumentUri = workspaceFile('foo.ts');
const editor = await createTestEditor(testDocumentUri,
`export abstract class Foo { abstract foo(): number; }`,
`class ConcreteFoo extends Foo { }`,
);
await wait(3000);
const fixes = await vscode.commands.executeCommand<vscode.CodeAction[]>('vscode.executeCodeActionProvider',
testDocumentUri,
editor.document.lineAt(1).range
);
assert.strictEqual(fixes?.length, 2);
assert.strictEqual(fixes![0].title, `Implement inherited abstract class`);
assert.strictEqual(fixes![1].title, `Remove unused declaration for: 'ConcreteFoo'`);
});
test('Add all missing imports should come after other add import fixes #98613', async () => {
await createTestEditor(workspaceFile('foo.ts'),
`export const foo = 1;`);
await createTestEditor(workspaceFile('bar.ts'),
`export const foo = 1;`);
const editor = await createTestEditor(workspaceFile('index.ts'),
`export const _ = 1;`,
`foo$0;`,
`foo$0;`
);
await wait(3000);
const fixes = await vscode.commands.executeCommand<vscode.CodeAction[]>('vscode.executeCodeActionProvider',
workspaceFile('index.ts'),
editor.document.lineAt(1).range
);
assert.strictEqual(fixes?.length, 3);
assert.strictEqual(fixes![0].title, `Import 'foo' from module "./bar"`);
assert.strictEqual(fixes![1].title, `Import 'foo' from module "./foo"`);
assert.strictEqual(fixes![2].title, `Add all missing imports`);
});
});
function workspaceFile(fileName: string) {
return vscode.Uri.joinPath(vscode.workspace.workspaceFolders![0].uri, fileName);
}

View File

@@ -0,0 +1,113 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { disposeAll } from '../utils/dispose';
import { createTestEditor, wait } from './testUtils';
type VsCodeConfiguration = { [key: string]: any };
async function updateConfig(newConfig: VsCodeConfiguration): Promise<VsCodeConfiguration> {
const oldConfig: VsCodeConfiguration = {};
const config = vscode.workspace.getConfiguration(undefined);
for (const configKey of Object.keys(newConfig)) {
oldConfig[configKey] = config.get(configKey);
await new Promise<void>((resolve, reject) =>
config.update(configKey, newConfig[configKey], vscode.ConfigurationTarget.Global)
.then(() => resolve(), reject));
}
return oldConfig;
}
namespace Config {
export const referencesCodeLens = 'typescript.referencesCodeLens.enabled';
}
suite('TypeScript References', () => {
const configDefaults: VsCodeConfiguration = Object.freeze({
[Config.referencesCodeLens]: true,
});
const _disposables: vscode.Disposable[] = [];
let oldConfig: { [key: string]: any } = {};
setup(async () => {
await wait(100);
// Save off config and apply defaults
oldConfig = await updateConfig(configDefaults);
});
teardown(async () => {
disposeAll(_disposables);
// Restore config
await updateConfig(oldConfig);
return vscode.commands.executeCommand('workbench.action.closeAllEditors');
});
test('Should show on basic class', async () => {
const testDocumentUri = vscode.Uri.parse('untitled:test1.ts');
await createTestEditor(testDocumentUri,
`class Foo {}`
);
const codeLenses = await getCodeLenses(testDocumentUri);
assert.strictEqual(codeLenses?.length, 1);
assert.strictEqual(codeLenses?.[0].range.start.line, 0);
});
test('Should show on basic class properties', async () => {
const testDocumentUri = vscode.Uri.parse('untitled:test2.ts');
await createTestEditor(testDocumentUri,
`class Foo {`,
` prop: number;`,
` meth(): void {}`,
`}`
);
const codeLenses = await getCodeLenses(testDocumentUri);
assert.strictEqual(codeLenses?.length, 3);
assert.strictEqual(codeLenses?.[0].range.start.line, 0);
assert.strictEqual(codeLenses?.[1].range.start.line, 1);
assert.strictEqual(codeLenses?.[2].range.start.line, 2);
});
test('Should not show on const property', async () => {
const testDocumentUri = vscode.Uri.parse('untitled:test3.ts');
await createTestEditor(testDocumentUri,
`const foo = {`,
` prop: 1;`,
` meth(): void {}`,
`}`
);
const codeLenses = await getCodeLenses(testDocumentUri);
assert.strictEqual(codeLenses?.length, 0);
});
test.skip('Should not show duplicate references on ES5 class (https://github.com/microsoft/vscode/issues/90396)', async () => {
const testDocumentUri = vscode.Uri.parse('untitled:test3.js');
await createTestEditor(testDocumentUri,
`function A() {`,
` console.log("hi");`,
`}`,
`A.x = {};`,
);
await wait(500);
const codeLenses = await getCodeLenses(testDocumentUri);
assert.strictEqual(codeLenses?.length, 1);
});
});
function getCodeLenses(document: vscode.Uri): Thenable<readonly vscode.CodeLens[] | undefined> {
return vscode.commands.executeCommand<readonly vscode.CodeLens[]>('vscode.executeCodeLensProvider', document, 100);
}

View File

@@ -0,0 +1,122 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import { RequestQueue, RequestQueueingType } from '../tsServer/requestQueue';
suite('RequestQueue', () => {
test('should be empty on creation', async () => {
const queue = new RequestQueue();
assert.strictEqual(queue.length, 0);
assert.strictEqual(queue.dequeue(), undefined);
});
suite('RequestQueue.createRequest', () => {
test('should create items with increasing sequence numbers', async () => {
const queue = new RequestQueue();
for (let i = 0; i < 100; ++i) {
const command = `command-${i}`;
const request = queue.createRequest(command, i);
assert.strictEqual(request.seq, i);
assert.strictEqual(request.command, command);
assert.strictEqual(request.arguments, i);
}
});
});
test('should queue normal requests in first in first out order', async () => {
const queue = new RequestQueue();
assert.strictEqual(queue.length, 0);
const request1 = queue.createRequest('a', 1);
queue.enqueue({ request: request1, expectsResponse: true, isAsync: false, queueingType: RequestQueueingType.Normal });
assert.strictEqual(queue.length, 1);
const request2 = queue.createRequest('b', 2);
queue.enqueue({ request: request2, expectsResponse: true, isAsync: false, queueingType: RequestQueueingType.Normal });
assert.strictEqual(queue.length, 2);
{
const item = queue.dequeue();
assert.strictEqual(queue.length, 1);
assert.strictEqual(item!.request.command, 'a');
}
{
const item = queue.dequeue();
assert.strictEqual(queue.length, 0);
assert.strictEqual(item!.request.command, 'b');
}
{
const item = queue.dequeue();
assert.strictEqual(item, undefined);
assert.strictEqual(queue.length, 0);
}
});
test('should put normal requests in front of low priority requests', async () => {
const queue = new RequestQueue();
assert.strictEqual(queue.length, 0);
queue.enqueue({ request: queue.createRequest('low-1', 1), expectsResponse: true, isAsync: false, queueingType: RequestQueueingType.LowPriority });
queue.enqueue({ request: queue.createRequest('low-2', 1), expectsResponse: true, isAsync: false, queueingType: RequestQueueingType.LowPriority });
queue.enqueue({ request: queue.createRequest('normal-1', 2), expectsResponse: true, isAsync: false, queueingType: RequestQueueingType.Normal });
queue.enqueue({ request: queue.createRequest('normal-2', 2), expectsResponse: true, isAsync: false, queueingType: RequestQueueingType.Normal });
{
const item = queue.dequeue();
assert.strictEqual(queue.length, 3);
assert.strictEqual(item!.request.command, 'normal-1');
}
{
const item = queue.dequeue();
assert.strictEqual(queue.length, 2);
assert.strictEqual(item!.request.command, 'normal-2');
}
{
const item = queue.dequeue();
assert.strictEqual(queue.length, 1);
assert.strictEqual(item!.request.command, 'low-1');
}
{
const item = queue.dequeue();
assert.strictEqual(queue.length, 0);
assert.strictEqual(item!.request.command, 'low-2');
}
});
test('should not push fence requests front of low priority requests', async () => {
const queue = new RequestQueue();
assert.strictEqual(queue.length, 0);
queue.enqueue({ request: queue.createRequest('low-1', 0), expectsResponse: true, isAsync: false, queueingType: RequestQueueingType.LowPriority });
queue.enqueue({ request: queue.createRequest('fence', 0), expectsResponse: true, isAsync: false, queueingType: RequestQueueingType.Fence });
queue.enqueue({ request: queue.createRequest('low-2', 0), expectsResponse: true, isAsync: false, queueingType: RequestQueueingType.LowPriority });
queue.enqueue({ request: queue.createRequest('normal', 0), expectsResponse: true, isAsync: false, queueingType: RequestQueueingType.Normal });
{
const item = queue.dequeue();
assert.strictEqual(queue.length, 3);
assert.strictEqual(item!.request.command, 'low-1');
}
{
const item = queue.dequeue();
assert.strictEqual(queue.length, 2);
assert.strictEqual(item!.request.command, 'fence');
}
{
const item = queue.dequeue();
assert.strictEqual(queue.length, 1);
assert.strictEqual(item!.request.command, 'normal');
}
{
const item = queue.dequeue();
assert.strictEqual(queue.length, 0);
assert.strictEqual(item!.request.command, 'low-2');
}
});
});

View File

@@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import * as stream from 'stream';
import type * as Proto from '../protocol';
import { NodeRequestCanceller } from '../tsServer/cancellation.electron';
import { ProcessBasedTsServer, TsServerProcess } from '../tsServer/server';
import { ServerType } from '../typescriptService';
import { nulToken } from '../utils/cancellation';
import { Logger } from '../utils/logger';
import { TelemetryReporter } from '../utils/telemetry';
import Tracer from '../utils/tracer';
const NoopTelemetryReporter = new class implements TelemetryReporter {
logTelemetry(): void { /* noop */ }
dispose(): void { /* noop */ }
};
class FakeServerProcess implements TsServerProcess {
private readonly _out: stream.PassThrough;
private readonly writeListeners = new Set<(data: Buffer) => void>();
public stdout: stream.PassThrough;
constructor() {
this._out = new stream.PassThrough();
this.stdout = this._out;
}
public write(data: Proto.Request) {
const listeners = Array.from(this.writeListeners);
this.writeListeners.clear();
setImmediate(() => {
for (const listener of listeners) {
listener(Buffer.from(JSON.stringify(data), 'utf8'));
}
const body = Buffer.from(JSON.stringify({ 'seq': data.seq, 'type': 'response', 'command': data.command, 'request_seq': data.seq, 'success': true }), 'utf8');
this._out.write(Buffer.from(`Content-Length: ${body.length}\r\n\r\n${body}`, 'utf8'));
});
}
onData(_handler: any) { /* noop */ }
onError(_handler: any) { /* noop */ }
onExit(_handler: any) { /* noop */ }
kill(): void { /* noop */ }
public onWrite(): Promise<any> {
return new Promise<string>((resolve) => {
this.writeListeners.add((data) => {
resolve(JSON.parse(data.toString()));
});
});
}
}
suite('Server', () => {
const tracer = new Tracer(new Logger());
test('should send requests with increasing sequence numbers', async () => {
const process = new FakeServerProcess();
const server = new ProcessBasedTsServer('semantic', ServerType.Semantic, process, undefined, new NodeRequestCanceller('semantic', tracer), undefined!, NoopTelemetryReporter, tracer);
const onWrite1 = process.onWrite();
server.executeImpl('geterr', {}, { isAsync: false, token: nulToken, expectsResult: true });
assert.strictEqual((await onWrite1).seq, 0);
const onWrite2 = process.onWrite();
server.executeImpl('geterr', {}, { isAsync: false, token: nulToken, expectsResult: true });
assert.strictEqual((await onWrite2).seq, 1);
});
});

View File

@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as vscode from 'vscode';
import { onChangedDocument, wait, retryUntilDocumentChanges } from './testUtils';
export async function acceptFirstSuggestion(uri: vscode.Uri, _disposables: vscode.Disposable[]) {
return retryUntilDocumentChanges(uri, { retries: 10, timeout: 0 }, _disposables, async () => {
await vscode.commands.executeCommand('editor.action.triggerSuggest');
await wait(1000);
await vscode.commands.executeCommand('acceptSelectedSuggestion');
});
}
export async function typeCommitCharacter(uri: vscode.Uri, character: string, _disposables: vscode.Disposable[]) {
const didChangeDocument = onChangedDocument(uri, _disposables);
await vscode.commands.executeCommand('editor.action.triggerSuggest');
await wait(3000); // Give time for suggestions to show
await vscode.commands.executeCommand('type', { text: character });
return await didChangeDocument;
}

View File

@@ -0,0 +1,173 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as fs from 'fs';
import * as os from 'os';
import { join } from 'path';
import * as vscode from 'vscode';
function rndName() {
return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10);
}
export function createRandomFile(contents = '', fileExtension = 'txt'): Thenable<vscode.Uri> {
return new Promise((resolve, reject) => {
const tmpFile = join(os.tmpdir(), rndName() + '.' + fileExtension);
fs.writeFile(tmpFile, contents, (error) => {
if (error) {
return reject(error);
}
resolve(vscode.Uri.file(tmpFile));
});
});
}
export function deleteFile(file: vscode.Uri): Thenable<boolean> {
return new Promise((resolve, reject) => {
fs.unlink(file.fsPath, (err) => {
if (err) {
reject(err);
} else {
resolve(true);
}
});
});
}
export const CURSOR = '$$CURSOR$$';
export function withRandomFileEditor(
contents: string,
fileExtension: string,
run: (editor: vscode.TextEditor, doc: vscode.TextDocument) => Thenable<void>
): Thenable<boolean> {
const cursorIndex = contents.indexOf(CURSOR);
return createRandomFile(contents.replace(CURSOR, ''), fileExtension).then(file => {
return vscode.workspace.openTextDocument(file).then(doc => {
return vscode.window.showTextDocument(doc).then((editor) => {
if (cursorIndex >= 0) {
const pos = doc.positionAt(cursorIndex);
editor.selection = new vscode.Selection(pos, pos);
}
return run(editor, doc).then(_ => {
if (doc.isDirty) {
return doc.save().then(() => {
return deleteFile(file);
});
} else {
return deleteFile(file);
}
});
});
});
});
}
export const wait = (ms: number) => new Promise<void>(resolve => setTimeout(() => resolve(), ms));
export const joinLines = (...args: string[]) => args.join(os.platform() === 'win32' ? '\r\n' : '\n');
export async function createTestEditor(uri: vscode.Uri, ...lines: string[]) {
const document = await vscode.workspace.openTextDocument(uri);
const editor = await vscode.window.showTextDocument(document);
await editor.insertSnippet(new vscode.SnippetString(joinLines(...lines)), new vscode.Range(0, 0, 1000, 0));
return editor;
}
export function assertEditorContents(editor: vscode.TextEditor, expectedDocContent: string, message?: string): void {
const cursorIndex = expectedDocContent.indexOf(CURSOR);
assert.strictEqual(
editor.document.getText(),
expectedDocContent.replace(CURSOR, ''),
message);
if (cursorIndex >= 0) {
const expectedCursorPos = editor.document.positionAt(cursorIndex);
assert.deepEqual(
{ line: editor.selection.active.line, character: editor.selection.active.line },
{ line: expectedCursorPos.line, character: expectedCursorPos.line },
'Cursor position'
);
}
}
export type VsCodeConfiguration = { [key: string]: any };
export async function updateConfig(documentUri: vscode.Uri, newConfig: VsCodeConfiguration): Promise<VsCodeConfiguration> {
const oldConfig: VsCodeConfiguration = {};
const config = vscode.workspace.getConfiguration(undefined, documentUri);
for (const configKey of Object.keys(newConfig)) {
oldConfig[configKey] = config.get(configKey);
await new Promise<void>((resolve, reject) =>
config.update(configKey, newConfig[configKey], vscode.ConfigurationTarget.Global)
.then(() => resolve(), reject));
}
return oldConfig;
}
export const Config = Object.freeze({
autoClosingBrackets: 'editor.autoClosingBrackets',
typescriptCompleteFunctionCalls: 'typescript.suggest.completeFunctionCalls',
insertMode: 'editor.suggest.insertMode',
snippetSuggestions: 'editor.snippetSuggestions',
suggestSelection: 'editor.suggestSelection',
javascriptQuoteStyle: 'javascript.preferences.quoteStyle',
typescriptQuoteStyle: 'typescript.preferences.quoteStyle',
} as const);
export const insertModesValues = Object.freeze(['insert', 'replace']);
export async function enumerateConfig(
documentUri: vscode.Uri,
configKey: string,
values: readonly string[],
f: (message: string) => Promise<void>
): Promise<void> {
for (const value of values) {
const newConfig = { [configKey]: value };
await updateConfig(documentUri, newConfig);
await f(JSON.stringify(newConfig));
}
}
export function onChangedDocument(documentUri: vscode.Uri, disposables: vscode.Disposable[]) {
return new Promise<vscode.TextDocument>(resolve => vscode.workspace.onDidChangeTextDocument(e => {
if (e.document.uri.toString() === documentUri.toString()) {
resolve(e.document);
}
}, undefined, disposables));
}
export async function retryUntilDocumentChanges(
documentUri: vscode.Uri,
options: { retries: number, timeout: number },
disposables: vscode.Disposable[],
exec: () => Thenable<unknown>,
) {
const didChangeDocument = onChangedDocument(documentUri, disposables);
let done = false;
const result = await Promise.race([
didChangeDocument,
(async () => {
for (let i = 0; i < options.retries; ++i) {
await wait(options.timeout);
if (done) {
return;
}
await exec();
}
})(),
]);
done = true;
return result;
}

View File

@@ -0,0 +1,619 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { ITypeScriptServiceClient, ClientCapability } from '../typescriptService';
import API from '../utils/api';
import { coalesce } from '../utils/arrays';
import { Delayer } from '../utils/async';
import { nulToken } from '../utils/cancellation';
import { Disposable } from '../utils/dispose';
import * as languageModeIds from '../utils/languageModeIds';
import { ResourceMap } from '../utils/resourceMap';
import * as typeConverters from '../utils/typeConverters';
const enum BufferKind {
TypeScript = 1,
JavaScript = 2,
}
const enum BufferState {
Initial = 1,
Open = 2,
Closed = 2,
}
function mode2ScriptKind(mode: string): 'TS' | 'TSX' | 'JS' | 'JSX' | undefined {
switch (mode) {
case languageModeIds.typescript: return 'TS';
case languageModeIds.typescriptreact: return 'TSX';
case languageModeIds.javascript: return 'JS';
case languageModeIds.javascriptreact: return 'JSX';
}
return undefined;
}
const enum BufferOperationType { Close, Open, Change }
class CloseOperation {
readonly type = BufferOperationType.Close;
constructor(
public readonly args: string
) { }
}
class OpenOperation {
readonly type = BufferOperationType.Open;
constructor(
public readonly args: Proto.OpenRequestArgs
) { }
}
class ChangeOperation {
readonly type = BufferOperationType.Change;
constructor(
public readonly args: Proto.FileCodeEdits
) { }
}
type BufferOperation = CloseOperation | OpenOperation | ChangeOperation;
/**
* Manages synchronization of buffers with the TS server.
*
* If supported, batches together file changes. This allows the TS server to more efficiently process changes.
*/
class BufferSynchronizer {
private readonly _pending: ResourceMap<BufferOperation>;
constructor(
private readonly client: ITypeScriptServiceClient,
onCaseInsenitiveFileSystem: boolean
) {
this._pending = new ResourceMap<BufferOperation>(undefined, {
onCaseInsenitiveFileSystem
});
}
public open(resource: vscode.Uri, args: Proto.OpenRequestArgs) {
if (this.supportsBatching) {
this.updatePending(resource, new OpenOperation(args));
} else {
this.client.executeWithoutWaitingForResponse('open', args);
}
}
/**
* @return Was the buffer open?
*/
public close(resource: vscode.Uri, filepath: string): boolean {
if (this.supportsBatching) {
return this.updatePending(resource, new CloseOperation(filepath));
} else {
const args: Proto.FileRequestArgs = { file: filepath };
this.client.executeWithoutWaitingForResponse('close', args);
return true;
}
}
public change(resource: vscode.Uri, filepath: string, events: readonly vscode.TextDocumentContentChangeEvent[]) {
if (!events.length) {
return;
}
if (this.supportsBatching) {
this.updatePending(resource, new ChangeOperation({
fileName: filepath,
textChanges: events.map((change): Proto.CodeEdit => ({
newText: change.text,
start: typeConverters.Position.toLocation(change.range.start),
end: typeConverters.Position.toLocation(change.range.end),
})).reverse(), // Send the edits end-of-document to start-of-document order
}));
} else {
for (const { range, text } of events) {
const args: Proto.ChangeRequestArgs = {
insertString: text,
...typeConverters.Range.toFormattingRequestArgs(filepath, range)
};
this.client.executeWithoutWaitingForResponse('change', args);
}
}
}
public reset(): void {
this._pending.clear();
}
public beforeCommand(command: string): void {
if (command === 'updateOpen') {
return;
}
this.flush();
}
private flush() {
if (!this.supportsBatching) {
// We've already eagerly synchronized
this._pending.clear();
return;
}
if (this._pending.size > 0) {
const closedFiles: string[] = [];
const openFiles: Proto.OpenRequestArgs[] = [];
const changedFiles: Proto.FileCodeEdits[] = [];
for (const change of this._pending.values) {
switch (change.type) {
case BufferOperationType.Change: changedFiles.push(change.args); break;
case BufferOperationType.Open: openFiles.push(change.args); break;
case BufferOperationType.Close: closedFiles.push(change.args); break;
}
}
this.client.execute('updateOpen', { changedFiles, closedFiles, openFiles }, nulToken, { nonRecoverable: true });
this._pending.clear();
}
}
private get supportsBatching(): boolean {
return this.client.apiVersion.gte(API.v340);
}
private updatePending(resource: vscode.Uri, op: BufferOperation): boolean {
switch (op.type) {
case BufferOperationType.Close:
const existing = this._pending.get(resource);
switch (existing?.type) {
case BufferOperationType.Open:
this._pending.delete(resource);
return false; // Open then close. No need to do anything
}
break;
}
if (this._pending.has(resource)) {
// we saw this file before, make sure we flush before working with it again
this.flush();
}
this._pending.set(resource, op);
return true;
}
}
class SyncedBuffer {
private state = BufferState.Initial;
constructor(
private readonly document: vscode.TextDocument,
public readonly filepath: string,
private readonly client: ITypeScriptServiceClient,
private readonly synchronizer: BufferSynchronizer,
) { }
public open(): void {
const args: Proto.OpenRequestArgs = {
file: this.filepath,
fileContent: this.document.getText(),
projectRootPath: this.client.getWorkspaceRootForResource(this.document.uri),
};
const scriptKind = mode2ScriptKind(this.document.languageId);
if (scriptKind) {
args.scriptKindName = scriptKind;
}
if (this.client.apiVersion.gte(API.v240)) {
const tsPluginsForDocument = this.client.pluginManager.plugins
.filter(x => x.languages.indexOf(this.document.languageId) >= 0);
if (tsPluginsForDocument.length) {
(args as any).plugins = tsPluginsForDocument.map(plugin => plugin.name);
}
}
this.synchronizer.open(this.resource, args);
this.state = BufferState.Open;
}
public get resource(): vscode.Uri {
return this.document.uri;
}
public get lineCount(): number {
return this.document.lineCount;
}
public get kind(): BufferKind {
switch (this.document.languageId) {
case languageModeIds.javascript:
case languageModeIds.javascriptreact:
return BufferKind.JavaScript;
case languageModeIds.typescript:
case languageModeIds.typescriptreact:
default:
return BufferKind.TypeScript;
}
}
/**
* @return Was the buffer open?
*/
public close(): boolean {
if (this.state !== BufferState.Open) {
this.state = BufferState.Closed;
return false;
}
this.state = BufferState.Closed;
return this.synchronizer.close(this.resource, this.filepath);
}
public onContentChanged(events: readonly vscode.TextDocumentContentChangeEvent[]): void {
if (this.state !== BufferState.Open) {
console.error(`Unexpected buffer state: ${this.state}`);
}
this.synchronizer.change(this.resource, this.filepath, events);
}
}
class SyncedBufferMap extends ResourceMap<SyncedBuffer> {
public getForPath(filePath: string): SyncedBuffer | undefined {
return this.get(vscode.Uri.file(filePath));
}
public get allBuffers(): Iterable<SyncedBuffer> {
return this.values;
}
}
class PendingDiagnostics extends ResourceMap<number> {
public getOrderedFileSet(): ResourceMap<void> {
const orderedResources = Array.from(this.entries)
.sort((a, b) => a.value - b.value)
.map(entry => entry.resource);
const map = new ResourceMap<void>(undefined, this.config);
for (const resource of orderedResources) {
map.set(resource, undefined);
}
return map;
}
}
class GetErrRequest {
public static executeGetErrRequest(
client: ITypeScriptServiceClient,
files: ResourceMap<void>,
onDone: () => void
) {
return new GetErrRequest(client, files, onDone);
}
private _done: boolean = false;
private readonly _token: vscode.CancellationTokenSource = new vscode.CancellationTokenSource();
private constructor(
client: ITypeScriptServiceClient,
public readonly files: ResourceMap<void>,
onDone: () => void
) {
const allFiles = coalesce(Array.from(files.entries)
.filter(entry => client.hasCapabilityForResource(entry.resource, ClientCapability.Semantic))
.map(entry => client.normalizedPath(entry.resource)));
if (!allFiles.length || !client.capabilities.has(ClientCapability.Semantic)) {
this._done = true;
setImmediate(onDone);
} else {
const request = client.configuration.enableProjectDiagnostics
// Note that geterrForProject is almost certainly not the api we want here as it ends up computing far
// too many diagnostics
? client.executeAsync('geterrForProject', { delay: 0, file: allFiles[0] }, this._token.token)
: client.executeAsync('geterr', { delay: 0, files: allFiles }, this._token.token);
request.finally(() => {
if (this._done) {
return;
}
this._done = true;
onDone();
});
}
}
public cancel(): any {
if (!this._done) {
this._token.cancel();
}
this._token.dispose();
}
}
export default class BufferSyncSupport extends Disposable {
private readonly client: ITypeScriptServiceClient;
private _validateJavaScript: boolean = true;
private _validateTypeScript: boolean = true;
private readonly modeIds: Set<string>;
private readonly syncedBuffers: SyncedBufferMap;
private readonly pendingDiagnostics: PendingDiagnostics;
private readonly diagnosticDelayer: Delayer<any>;
private pendingGetErr: GetErrRequest | undefined;
private listening: boolean = false;
private readonly synchronizer: BufferSynchronizer;
constructor(
client: ITypeScriptServiceClient,
modeIds: readonly string[],
onCaseInsenitiveFileSystem: boolean
) {
super();
this.client = client;
this.modeIds = new Set<string>(modeIds);
this.diagnosticDelayer = new Delayer<any>(300);
const pathNormalizer = (path: vscode.Uri) => this.client.normalizedPath(path);
this.syncedBuffers = new SyncedBufferMap(pathNormalizer, { onCaseInsenitiveFileSystem });
this.pendingDiagnostics = new PendingDiagnostics(pathNormalizer, { onCaseInsenitiveFileSystem });
this.synchronizer = new BufferSynchronizer(client, onCaseInsenitiveFileSystem);
this.updateConfiguration();
vscode.workspace.onDidChangeConfiguration(this.updateConfiguration, this, this._disposables);
}
private readonly _onDelete = this._register(new vscode.EventEmitter<vscode.Uri>());
public readonly onDelete = this._onDelete.event;
private readonly _onWillChange = this._register(new vscode.EventEmitter<vscode.Uri>());
public readonly onWillChange = this._onWillChange.event;
public listen(): void {
if (this.listening) {
return;
}
this.listening = true;
vscode.workspace.onDidOpenTextDocument(this.openTextDocument, this, this._disposables);
vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, this._disposables);
vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, this._disposables);
vscode.window.onDidChangeVisibleTextEditors(e => {
for (const { document } of e) {
const syncedBuffer = this.syncedBuffers.get(document.uri);
if (syncedBuffer) {
this.requestDiagnostic(syncedBuffer);
}
}
}, this, this._disposables);
vscode.workspace.textDocuments.forEach(this.openTextDocument, this);
}
public handles(resource: vscode.Uri): boolean {
return this.syncedBuffers.has(resource);
}
public ensureHasBuffer(resource: vscode.Uri): boolean {
if (this.syncedBuffers.has(resource)) {
return true;
}
const existingDocument = vscode.workspace.textDocuments.find(doc => doc.uri.toString() === resource.toString());
if (existingDocument) {
return this.openTextDocument(existingDocument);
}
return false;
}
public toVsCodeResource(resource: vscode.Uri): vscode.Uri {
const filepath = this.client.normalizedPath(resource);
for (const buffer of this.syncedBuffers.allBuffers) {
if (buffer.filepath === filepath) {
return buffer.resource;
}
}
return resource;
}
public toResource(filePath: string): vscode.Uri {
const buffer = this.syncedBuffers.getForPath(filePath);
if (buffer) {
return buffer.resource;
}
return vscode.Uri.file(filePath);
}
public reset(): void {
this.pendingGetErr?.cancel();
this.pendingDiagnostics.clear();
this.synchronizer.reset();
}
public reinitialize(): void {
this.reset();
for (const buffer of this.syncedBuffers.allBuffers) {
buffer.open();
}
}
public openTextDocument(document: vscode.TextDocument): boolean {
if (!this.modeIds.has(document.languageId)) {
return false;
}
const resource = document.uri;
const filepath = this.client.normalizedPath(resource);
if (!filepath) {
return false;
}
if (this.syncedBuffers.has(resource)) {
return true;
}
const syncedBuffer = new SyncedBuffer(document, filepath, this.client, this.synchronizer);
this.syncedBuffers.set(resource, syncedBuffer);
syncedBuffer.open();
this.requestDiagnostic(syncedBuffer);
return true;
}
public closeResource(resource: vscode.Uri): void {
const syncedBuffer = this.syncedBuffers.get(resource);
if (!syncedBuffer) {
return;
}
this.pendingDiagnostics.delete(resource);
this.pendingGetErr?.files.delete(resource);
this.syncedBuffers.delete(resource);
const wasBufferOpen = syncedBuffer.close();
this._onDelete.fire(resource);
if (wasBufferOpen) {
this.requestAllDiagnostics();
}
}
public interuptGetErr<R>(f: () => R): R {
if (!this.pendingGetErr
|| this.client.configuration.enableProjectDiagnostics // `geterr` happens on seperate server so no need to cancel it.
) {
return f();
}
this.pendingGetErr.cancel();
this.pendingGetErr = undefined;
const result = f();
this.triggerDiagnostics();
return result;
}
public beforeCommand(command: string): void {
this.synchronizer.beforeCommand(command);
}
private onDidCloseTextDocument(document: vscode.TextDocument): void {
this.closeResource(document.uri);
}
private onDidChangeTextDocument(e: vscode.TextDocumentChangeEvent): void {
const syncedBuffer = this.syncedBuffers.get(e.document.uri);
if (!syncedBuffer) {
return;
}
this._onWillChange.fire(syncedBuffer.resource);
syncedBuffer.onContentChanged(e.contentChanges);
const didTrigger = this.requestDiagnostic(syncedBuffer);
if (!didTrigger && this.pendingGetErr) {
// In this case we always want to re-trigger all diagnostics
this.pendingGetErr.cancel();
this.pendingGetErr = undefined;
this.triggerDiagnostics();
}
}
public requestAllDiagnostics() {
for (const buffer of this.syncedBuffers.allBuffers) {
if (this.shouldValidate(buffer)) {
this.pendingDiagnostics.set(buffer.resource, Date.now());
}
}
this.triggerDiagnostics();
}
public getErr(resources: readonly vscode.Uri[]): any {
const handledResources = resources.filter(resource => this.handles(resource));
if (!handledResources.length) {
return;
}
for (const resource of handledResources) {
this.pendingDiagnostics.set(resource, Date.now());
}
this.triggerDiagnostics();
}
private triggerDiagnostics(delay: number = 200) {
this.diagnosticDelayer.trigger(() => {
this.sendPendingDiagnostics();
}, delay);
}
private requestDiagnostic(buffer: SyncedBuffer): boolean {
if (!this.shouldValidate(buffer)) {
return false;
}
this.pendingDiagnostics.set(buffer.resource, Date.now());
const delay = Math.min(Math.max(Math.ceil(buffer.lineCount / 20), 300), 800);
this.triggerDiagnostics(delay);
return true;
}
public hasPendingDiagnostics(resource: vscode.Uri): boolean {
return this.pendingDiagnostics.has(resource);
}
private sendPendingDiagnostics(): void {
const orderedFileSet = this.pendingDiagnostics.getOrderedFileSet();
if (this.pendingGetErr) {
this.pendingGetErr.cancel();
for (const { resource } of this.pendingGetErr.files.entries) {
if (this.syncedBuffers.get(resource)) {
orderedFileSet.set(resource, undefined);
}
}
this.pendingGetErr = undefined;
}
// Add all open TS buffers to the geterr request. They might be visible
for (const buffer of this.syncedBuffers.values) {
orderedFileSet.set(buffer.resource, undefined);
}
if (orderedFileSet.size) {
const getErr = this.pendingGetErr = GetErrRequest.executeGetErrRequest(this.client, orderedFileSet, () => {
if (this.pendingGetErr === getErr) {
this.pendingGetErr = undefined;
}
});
}
this.pendingDiagnostics.clear();
}
private updateConfiguration() {
const jsConfig = vscode.workspace.getConfiguration('javascript', null);
const tsConfig = vscode.workspace.getConfiguration('typescript', null);
this._validateJavaScript = jsConfig.get<boolean>('validate.enable', true);
this._validateTypeScript = tsConfig.get<boolean>('validate.enable', true);
}
private shouldValidate(buffer: SyncedBuffer) {
switch (buffer.kind) {
case BufferKind.JavaScript:
return this._validateJavaScript;
case BufferKind.TypeScript:
default:
return this._validateTypeScript;
}
}
}

View File

@@ -0,0 +1,48 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { ServerResponse } from '../typescriptService';
type Resolve<T extends Proto.Response> = () => Promise<ServerResponse.Response<T>>;
/**
* Caches a class of TS Server request based on document.
*/
export class CachedResponse<T extends Proto.Response> {
private response?: Promise<ServerResponse.Response<T>>;
private version: number = -1;
private document: string = '';
/**
* Execute a request. May return cached value or resolve the new value
*
* Caller must ensure that all input `resolve` functions return equivilent results (keyed only off of document).
*/
public execute(
document: vscode.TextDocument,
resolve: Resolve<T>
): Promise<ServerResponse.Response<T>> {
if (this.response && this.matches(document)) {
// Chain so that on cancellation we fall back to the next resolve
return this.response = this.response.then(result => result.type === 'cancelled' ? resolve() : result);
}
return this.reset(document, resolve);
}
private matches(document: vscode.TextDocument): boolean {
return this.version === document.version && this.document === document.uri.toString();
}
private async reset(
document: vscode.TextDocument,
resolve: Resolve<T>
): Promise<ServerResponse.Response<T>> {
this.version = document.version;
this.document = document.uri.toString();
return this.response = resolve();
}
}

View File

@@ -0,0 +1,51 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as Proto from '../protocol';
import { ServerResponse } from '../typescriptService';
export interface CallbackItem<R> {
readonly onSuccess: (value: R) => void;
readonly onError: (err: Error) => void;
readonly queuingStartTime: number;
readonly isAsync: boolean;
}
export class CallbackMap<R extends Proto.Response> {
private readonly _callbacks = new Map<number, CallbackItem<ServerResponse.Response<R> | undefined>>();
private readonly _asyncCallbacks = new Map<number, CallbackItem<ServerResponse.Response<R> | undefined>>();
public destroy(cause: string): void {
const cancellation = new ServerResponse.Cancelled(cause);
for (const callback of this._callbacks.values()) {
callback.onSuccess(cancellation);
}
this._callbacks.clear();
for (const callback of this._asyncCallbacks.values()) {
callback.onSuccess(cancellation);
}
this._asyncCallbacks.clear();
}
public add(seq: number, callback: CallbackItem<ServerResponse.Response<R> | undefined>, isAsync: boolean) {
if (isAsync) {
this._asyncCallbacks.set(seq, callback);
} else {
this._callbacks.set(seq, callback);
}
}
public fetch(seq: number): CallbackItem<ServerResponse.Response<R> | undefined> | undefined {
const callback = this._callbacks.get(seq) || this._asyncCallbacks.get(seq);
this.delete(seq);
return callback;
}
private delete(seq: number) {
if (!this._callbacks.delete(seq)) {
this._asyncCallbacks.delete(seq);
}
}
}

View File

@@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import { getTempFile } from '../utils/temp.electron';
import Tracer from '../utils/tracer';
import { OngoingRequestCanceller, OngoingRequestCancellerFactory } from './cancellation';
export class NodeRequestCanceller implements OngoingRequestCanceller {
public readonly cancellationPipeName: string;
public constructor(
private readonly _serverId: string,
private readonly _tracer: Tracer,
) {
this.cancellationPipeName = getTempFile('tscancellation');
}
public tryCancelOngoingRequest(seq: number): boolean {
if (!this.cancellationPipeName) {
return false;
}
this._tracer.logTrace(this._serverId, `TypeScript Server: trying to cancel ongoing request with sequence number ${seq}`);
try {
fs.writeFileSync(this.cancellationPipeName + seq, '');
} catch {
// noop
}
return true;
}
}
export const nodeRequestCancellerFactory = new class implements OngoingRequestCancellerFactory {
create(serverId: string, tracer: Tracer): OngoingRequestCanceller {
return new NodeRequestCanceller(serverId, tracer);
}
};

View File

@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import Tracer from '../utils/tracer';
export interface OngoingRequestCanceller {
readonly cancellationPipeName: string | undefined;
tryCancelOngoingRequest(seq: number): boolean;
}
export interface OngoingRequestCancellerFactory {
create(serverId: string, tracer: Tracer): OngoingRequestCanceller;
}
const noopRequestCanceller = new class implements OngoingRequestCanceller {
public readonly cancellationPipeName = undefined;
public tryCancelOngoingRequest(_seq: number): boolean {
return false;
}
};
export const noopRequestCancellerFactory = new class implements OngoingRequestCancellerFactory {
create(_serverId: string, _tracer: Tracer): OngoingRequestCanceller {
return noopRequestCanceller;
}
};

View File

@@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
import { ILogDirectoryProvider } from './logDirectoryProvider';
import { memoize } from '../utils/memoize';
export class NodeLogDirectoryProvider implements ILogDirectoryProvider {
public constructor(
private readonly context: vscode.ExtensionContext
) { }
public getNewLogDirectory(): string | undefined {
const root = this.logDirectory();
if (root) {
try {
return fs.mkdtempSync(path.join(root, `tsserver-log-`));
} catch (e) {
return undefined;
}
}
return undefined;
}
@memoize
private logDirectory(): string | undefined {
try {
const path = this.context.logPath;
if (!fs.existsSync(path)) {
fs.mkdirSync(path);
}
return this.context.logPath;
} catch {
return undefined;
}
}
}

View File

@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface ILogDirectoryProvider {
getNewLogDirectory(): string | undefined;
}
export const noopLogDirectoryProvider = new class implements ILogDirectoryProvider {
public getNewLogDirectory(): undefined {
return undefined;
}
};

View File

@@ -0,0 +1,81 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as Proto from '../protocol';
export enum RequestQueueingType {
/**
* Normal request that is executed in order.
*/
Normal = 1,
/**
* Request that normal requests jump in front of in the queue.
*/
LowPriority = 2,
/**
* A fence that blocks request reordering.
*
* Fences are not reordered. Unlike a normal request, a fence will never jump in front of a low priority request
* in the request queue.
*/
Fence = 3,
}
export interface RequestItem {
readonly request: Proto.Request;
readonly expectsResponse: boolean;
readonly isAsync: boolean;
readonly queueingType: RequestQueueingType;
}
export class RequestQueue {
private readonly queue: RequestItem[] = [];
private sequenceNumber: number = 0;
public get length(): number {
return this.queue.length;
}
public enqueue(item: RequestItem): void {
if (item.queueingType === RequestQueueingType.Normal) {
let index = this.queue.length - 1;
while (index >= 0) {
if (this.queue[index].queueingType !== RequestQueueingType.LowPriority) {
break;
}
--index;
}
this.queue.splice(index + 1, 0, item);
} else {
// Only normal priority requests can be reordered. All other requests just go to the end.
this.queue.push(item);
}
}
public dequeue(): RequestItem | undefined {
return this.queue.shift();
}
public tryDeletePendingRequest(seq: number): boolean {
for (let i = 0; i < this.queue.length; i++) {
if (this.queue[i].request.seq === seq) {
this.queue.splice(i, 1);
return true;
}
}
return false;
}
public createRequest(command: string, args: any): Proto.Request {
return {
seq: this.sequenceNumber++,
type: 'request',
command: command,
arguments: args
};
}
}

View File

@@ -0,0 +1,628 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { EventName } from '../protocol.const';
import { CallbackMap } from '../tsServer/callbackMap';
import { RequestItem, RequestQueue, RequestQueueingType } from '../tsServer/requestQueue';
import { TypeScriptServerError } from '../tsServer/serverError';
import { ServerResponse, ServerType, TypeScriptRequests } from '../typescriptService';
import { TypeScriptServiceConfiguration } from '../utils/configuration';
import { Disposable } from '../utils/dispose';
import { TelemetryReporter } from '../utils/telemetry';
import Tracer from '../utils/tracer';
import { OngoingRequestCanceller } from './cancellation';
import { TypeScriptVersionManager } from './versionManager';
import { TypeScriptVersion } from './versionProvider';
export enum ExectuionTarget {
Semantic,
Syntax
}
export interface ITypeScriptServer {
readonly onEvent: vscode.Event<Proto.Event>;
readonly onExit: vscode.Event<any>;
readonly onError: vscode.Event<any>;
readonly tsServerLogFile: string | undefined;
kill(): void;
executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean, executionTarget?: ExectuionTarget }): undefined;
executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise<ServerResponse.Response<Proto.Response>>;
executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise<ServerResponse.Response<Proto.Response>> | undefined;
dispose(): void;
}
export interface TsServerDelegate {
onFatalError(command: string, error: Error): void;
}
export const enum TsServerProcessKind {
Main = 'main',
Syntax = 'syntax',
Semantic = 'semantic',
Diagnostics = 'diagnostics'
}
export interface TsServerProcessFactory {
fork(
tsServerPath: string,
args: readonly string[],
kind: TsServerProcessKind,
configuration: TypeScriptServiceConfiguration,
versionManager: TypeScriptVersionManager,
): TsServerProcess;
}
export interface TsServerProcess {
write(serverRequest: Proto.Request): void;
onData(handler: (data: Proto.Response) => void): void;
onExit(handler: (code: number | null) => void): void;
onError(handler: (error: Error) => void): void;
kill(): void;
}
export class ProcessBasedTsServer extends Disposable implements ITypeScriptServer {
private readonly _requestQueue = new RequestQueue();
private readonly _callbacks = new CallbackMap<Proto.Response>();
private readonly _pendingResponses = new Set<number>();
constructor(
private readonly _serverId: string,
private readonly _serverSource: ServerType,
private readonly _process: TsServerProcess,
private readonly _tsServerLogFile: string | undefined,
private readonly _requestCanceller: OngoingRequestCanceller,
private readonly _version: TypeScriptVersion,
private readonly _telemetryReporter: TelemetryReporter,
private readonly _tracer: Tracer,
) {
super();
this._process.onData(msg => {
this.dispatchMessage(msg);
});
this._process.onExit(code => {
this._onExit.fire(code);
this._callbacks.destroy('server exited');
});
this._process.onError(error => {
this._onError.fire(error);
this._callbacks.destroy('server errored');
});
}
private readonly _onEvent = this._register(new vscode.EventEmitter<Proto.Event>());
public readonly onEvent = this._onEvent.event;
private readonly _onExit = this._register(new vscode.EventEmitter<any>());
public readonly onExit = this._onExit.event;
private readonly _onError = this._register(new vscode.EventEmitter<any>());
public readonly onError = this._onError.event;
public get tsServerLogFile() { return this._tsServerLogFile; }
private write(serverRequest: Proto.Request) {
this._process.write(serverRequest);
}
public dispose() {
super.dispose();
this._callbacks.destroy('server disposed');
this._pendingResponses.clear();
}
public kill() {
this._process.kill();
}
private dispatchMessage(message: Proto.Message) {
try {
switch (message.type) {
case 'response':
if (this._serverSource) {
this.dispatchResponse({
...(message as Proto.Response),
_serverType: this._serverSource
});
} else {
this.dispatchResponse(message as Proto.Response);
}
break;
case 'event':
const event = message as Proto.Event;
if (event.event === 'requestCompleted') {
const seq = (event as Proto.RequestCompletedEvent).body.request_seq;
const callback = this._callbacks.fetch(seq);
if (callback) {
this._tracer.traceRequestCompleted(this._serverId, 'requestCompleted', seq, callback);
callback.onSuccess(undefined);
}
} else {
this._tracer.traceEvent(this._serverId, event);
this._onEvent.fire(event);
}
break;
default:
throw new Error(`Unknown message type ${message.type} received`);
}
} finally {
this.sendNextRequests();
}
}
private tryCancelRequest(seq: number, command: string): boolean {
try {
if (this._requestQueue.tryDeletePendingRequest(seq)) {
this.logTrace(`Canceled request with sequence number ${seq}`);
return true;
}
if (this._requestCanceller.tryCancelOngoingRequest(seq)) {
return true;
}
this.logTrace(`Tried to cancel request with sequence number ${seq}. But request got already delivered.`);
return false;
} finally {
const callback = this.fetchCallback(seq);
if (callback) {
callback.onSuccess(new ServerResponse.Cancelled(`Cancelled request ${seq} - ${command}`));
}
}
}
private dispatchResponse(response: Proto.Response) {
const callback = this.fetchCallback(response.request_seq);
if (!callback) {
return;
}
this._tracer.traceResponse(this._serverId, response, callback);
if (response.success) {
callback.onSuccess(response);
} else if (response.message === 'No content available.') {
// Special case where response itself is successful but there is not any data to return.
callback.onSuccess(ServerResponse.NoContent);
} else {
callback.onError(TypeScriptServerError.create(this._serverId, this._version, response));
}
}
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean, executionTarget?: ExectuionTarget }): undefined;
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise<ServerResponse.Response<Proto.Response>>;
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise<ServerResponse.Response<Proto.Response>> | undefined {
const request = this._requestQueue.createRequest(command, args);
const requestInfo: RequestItem = {
request,
expectsResponse: executeInfo.expectsResult,
isAsync: executeInfo.isAsync,
queueingType: ProcessBasedTsServer.getQueueingType(command, executeInfo.lowPriority)
};
let result: Promise<ServerResponse.Response<Proto.Response>> | undefined;
if (executeInfo.expectsResult) {
result = new Promise<ServerResponse.Response<Proto.Response>>((resolve, reject) => {
this._callbacks.add(request.seq, { onSuccess: resolve as () => ServerResponse.Response<Proto.Response> | undefined, onError: reject, queuingStartTime: Date.now(), isAsync: executeInfo.isAsync }, executeInfo.isAsync);
if (executeInfo.token) {
executeInfo.token.onCancellationRequested(() => {
this.tryCancelRequest(request.seq, command);
});
}
}).catch((err: Error) => {
if (err instanceof TypeScriptServerError) {
if (!executeInfo.token || !executeInfo.token.isCancellationRequested) {
/* __GDPR__
"languageServiceErrorResponse" : {
"${include}": [
"${TypeScriptCommonProperties}",
"${TypeScriptRequestErrorProperties}"
]
}
*/
this._telemetryReporter.logTelemetry('languageServiceErrorResponse', err.telemetry);
}
}
throw err;
});
}
this._requestQueue.enqueue(requestInfo);
this.sendNextRequests();
return result;
}
private sendNextRequests(): void {
while (this._pendingResponses.size === 0 && this._requestQueue.length > 0) {
const item = this._requestQueue.dequeue();
if (item) {
this.sendRequest(item);
}
}
}
private sendRequest(requestItem: RequestItem): void {
const serverRequest = requestItem.request;
this._tracer.traceRequest(this._serverId, serverRequest, requestItem.expectsResponse, this._requestQueue.length);
if (requestItem.expectsResponse && !requestItem.isAsync) {
this._pendingResponses.add(requestItem.request.seq);
}
try {
this.write(serverRequest);
} catch (err) {
const callback = this.fetchCallback(serverRequest.seq);
if (callback) {
callback.onError(err);
}
}
}
private fetchCallback(seq: number) {
const callback = this._callbacks.fetch(seq);
if (!callback) {
return undefined;
}
this._pendingResponses.delete(seq);
return callback;
}
private logTrace(message: string) {
this._tracer.logTrace(this._serverId, message);
}
private static readonly fenceCommands = new Set(['change', 'close', 'open', 'updateOpen']);
private static getQueueingType(
command: string,
lowPriority?: boolean
): RequestQueueingType {
if (ProcessBasedTsServer.fenceCommands.has(command)) {
return RequestQueueingType.Fence;
}
return lowPriority ? RequestQueueingType.LowPriority : RequestQueueingType.Normal;
}
}
interface ExecuteInfo {
readonly isAsync: boolean;
readonly token?: vscode.CancellationToken;
readonly expectsResult: boolean;
readonly lowPriority?: boolean;
readonly executionTarget?: ExectuionTarget;
}
class RequestRouter {
private static readonly sharedCommands = new Set<keyof TypeScriptRequests>([
'change',
'close',
'open',
'updateOpen',
'configure',
]);
constructor(
private readonly servers: ReadonlyArray<{
readonly server: ITypeScriptServer;
canRun?(command: keyof TypeScriptRequests, executeInfo: ExecuteInfo): void;
}>,
private readonly delegate: TsServerDelegate,
) { }
public execute(command: keyof TypeScriptRequests, args: any, executeInfo: ExecuteInfo): Promise<ServerResponse.Response<Proto.Response>> | undefined {
if (RequestRouter.sharedCommands.has(command) && typeof executeInfo.executionTarget === 'undefined') {
// Dispatch shared commands to all servers but only return from first one
const requestStates: RequestState.State[] = this.servers.map(() => RequestState.Unresolved);
// Also make sure we never cancel requests to just one server
let token: vscode.CancellationToken | undefined = undefined;
if (executeInfo.token) {
const source = new vscode.CancellationTokenSource();
executeInfo.token.onCancellationRequested(() => {
if (requestStates.some(state => state === RequestState.Resolved)) {
// Don't cancel.
// One of the servers completed this request so we don't want to leave the other
// in a different state.
return;
}
source.cancel();
});
token = source.token;
}
let firstRequest: Promise<ServerResponse.Response<Proto.Response>> | undefined;
for (let serverIndex = 0; serverIndex < this.servers.length; ++serverIndex) {
const server = this.servers[serverIndex].server;
const request = server.executeImpl(command, args, { ...executeInfo, token });
if (serverIndex === 0) {
firstRequest = request;
}
if (request) {
request
.then(result => {
requestStates[serverIndex] = RequestState.Resolved;
const erroredRequest = requestStates.find(state => state.type === RequestState.Type.Errored) as RequestState.Errored | undefined;
if (erroredRequest) {
// We've gone out of sync
this.delegate.onFatalError(command, erroredRequest.err);
}
return result;
}, err => {
requestStates[serverIndex] = new RequestState.Errored(err);
if (requestStates.some(state => state === RequestState.Resolved)) {
// We've gone out of sync
this.delegate.onFatalError(command, err);
}
throw err;
});
}
}
return firstRequest;
}
for (const { canRun, server } of this.servers) {
if (!canRun || canRun(command, executeInfo)) {
return server.executeImpl(command, args, executeInfo);
}
}
throw new Error(`Could not find server for command: '${command}'`);
}
}
export class GetErrRoutingTsServer extends Disposable implements ITypeScriptServer {
private static readonly diagnosticEvents = new Set<string>([
EventName.configFileDiag,
EventName.syntaxDiag,
EventName.semanticDiag,
EventName.suggestionDiag
]);
private readonly getErrServer: ITypeScriptServer;
private readonly mainServer: ITypeScriptServer;
private readonly router: RequestRouter;
public constructor(
servers: { getErr: ITypeScriptServer, primary: ITypeScriptServer },
delegate: TsServerDelegate,
) {
super();
this.getErrServer = servers.getErr;
this.mainServer = servers.primary;
this.router = new RequestRouter(
[
{ server: this.getErrServer, canRun: (command) => ['geterr', 'geterrForProject'].includes(command) },
{ server: this.mainServer, canRun: undefined /* gets all other commands */ }
],
delegate);
this._register(this.getErrServer.onEvent(e => {
if (GetErrRoutingTsServer.diagnosticEvents.has(e.event)) {
this._onEvent.fire(e);
}
// Ignore all other events
}));
this._register(this.mainServer.onEvent(e => {
if (!GetErrRoutingTsServer.diagnosticEvents.has(e.event)) {
this._onEvent.fire(e);
}
// Ignore all other events
}));
this._register(this.getErrServer.onError(e => this._onError.fire(e)));
this._register(this.mainServer.onError(e => this._onError.fire(e)));
this._register(this.mainServer.onExit(e => {
this._onExit.fire(e);
this.getErrServer.kill();
}));
}
private readonly _onEvent = this._register(new vscode.EventEmitter<Proto.Event>());
public readonly onEvent = this._onEvent.event;
private readonly _onExit = this._register(new vscode.EventEmitter<any>());
public readonly onExit = this._onExit.event;
private readonly _onError = this._register(new vscode.EventEmitter<any>());
public readonly onError = this._onError.event;
public get tsServerLogFile() { return this.mainServer.tsServerLogFile; }
public kill(): void {
this.getErrServer.kill();
this.mainServer.kill();
}
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean, executionTarget?: ExectuionTarget }): undefined;
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise<ServerResponse.Response<Proto.Response>>;
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise<ServerResponse.Response<Proto.Response>> | undefined {
return this.router.execute(command, args, executeInfo);
}
}
export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServer {
/**
* Commands that should always be run on the syntax server.
*/
private static readonly syntaxAlwaysCommands = new Set<keyof TypeScriptRequests>([
'navtree',
'getOutliningSpans',
'jsxClosingTag',
'selectionRange',
'format',
'formatonkey',
'docCommentTemplate',
]);
/**
* Commands that should always be run on the semantic server.
*/
private static readonly semanticCommands = new Set<keyof TypeScriptRequests>([
'geterr',
'geterrForProject',
'projectInfo',
'configurePlugin',
]);
/**
* Commands that can be run on the syntax server but would benefit from being upgraded to the semantic server.
*/
private static readonly syntaxAllowedCommands = new Set<keyof TypeScriptRequests>([
'completions',
'completionEntryDetails',
'completionInfo',
'definition',
'definitionAndBoundSpan',
'documentHighlights',
'implementation',
'navto',
'quickinfo',
'references',
'rename',
'signatureHelp',
]);
private readonly syntaxServer: ITypeScriptServer;
private readonly semanticServer: ITypeScriptServer;
private readonly router: RequestRouter;
private _projectLoading = true;
public constructor(
servers: { syntax: ITypeScriptServer, semantic: ITypeScriptServer },
delegate: TsServerDelegate,
enableDynamicRouting: boolean,
) {
super();
this.syntaxServer = servers.syntax;
this.semanticServer = servers.semantic;
this.router = new RequestRouter(
[
{
server: this.syntaxServer,
canRun: (command, execInfo) => {
switch (execInfo.executionTarget) {
case ExectuionTarget.Semantic: return false;
case ExectuionTarget.Syntax: return true;
}
if (SyntaxRoutingTsServer.syntaxAlwaysCommands.has(command)) {
return true;
}
if (SyntaxRoutingTsServer.semanticCommands.has(command)) {
return false;
}
if (enableDynamicRouting && this.projectLoading && SyntaxRoutingTsServer.syntaxAllowedCommands.has(command)) {
return true;
}
return false;
}
}, {
server: this.semanticServer,
canRun: undefined /* gets all other commands */
}
],
delegate);
this._register(this.syntaxServer.onEvent(e => {
return this._onEvent.fire(e);
}));
this._register(this.semanticServer.onEvent(e => {
switch (e.event) {
case EventName.projectLoadingStart:
this._projectLoading = true;
break;
case EventName.projectLoadingFinish:
case EventName.semanticDiag:
case EventName.syntaxDiag:
case EventName.suggestionDiag:
case EventName.configFileDiag:
this._projectLoading = false;
break;
}
return this._onEvent.fire(e);
}));
this._register(this.semanticServer.onExit(e => {
this._onExit.fire(e);
this.syntaxServer.kill();
}));
this._register(this.semanticServer.onError(e => this._onError.fire(e)));
}
private get projectLoading() { return this._projectLoading; }
private readonly _onEvent = this._register(new vscode.EventEmitter<Proto.Event>());
public readonly onEvent = this._onEvent.event;
private readonly _onExit = this._register(new vscode.EventEmitter<any>());
public readonly onExit = this._onExit.event;
private readonly _onError = this._register(new vscode.EventEmitter<any>());
public readonly onError = this._onError.event;
public get tsServerLogFile() { return this.semanticServer.tsServerLogFile; }
public kill(): void {
this.syntaxServer.kill();
this.semanticServer.kill();
}
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean, executionTarget?: ExectuionTarget }): undefined;
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise<ServerResponse.Response<Proto.Response>>;
public executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, executionTarget?: ExectuionTarget }): Promise<ServerResponse.Response<Proto.Response>> | undefined {
return this.router.execute(command, args, executeInfo);
}
}
namespace RequestState {
export const enum Type { Unresolved, Resolved, Errored }
export const Unresolved = { type: Type.Unresolved } as const;
export const Resolved = { type: Type.Resolved } as const;
export class Errored {
readonly type = Type.Errored;
constructor(
public readonly err: Error
) { }
}
export type State = typeof Unresolved | typeof Resolved | Errored;
}

View File

@@ -0,0 +1,97 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as Proto from '../protocol';
import { TypeScriptVersion } from './versionProvider';
export class TypeScriptServerError extends Error {
public static create(
serverId: string,
version: TypeScriptVersion,
response: Proto.Response
): TypeScriptServerError {
const parsedResult = TypeScriptServerError.parseErrorText(response);
return new TypeScriptServerError(serverId, version, response, parsedResult?.message, parsedResult?.stack, parsedResult?.sanitizedStack);
}
private constructor(
public readonly serverId: string,
public readonly version: TypeScriptVersion,
private readonly response: Proto.Response,
public readonly serverMessage: string | undefined,
public readonly serverStack: string | undefined,
private readonly sanitizedStack: string | undefined
) {
super(`<${serverId}> TypeScript Server Error (${version.displayName})\n${serverMessage}\n${serverStack}`);
}
public get serverErrorText() { return this.response.message; }
public get serverCommand() { return this.response.command; }
public get telemetry() {
// The "sanitizedstack" has been purged of error messages, paths, and file names (other than tsserver)
// and, thus, can be classified as SystemMetaData, rather than CallstackOrException.
/* __GDPR__FRAGMENT__
"TypeScriptRequestErrorProperties" : {
"command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"serverid" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" },
"sanitizedstack" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }
}
*/
return {
command: this.serverCommand,
serverid: this.serverId,
sanitizedstack: this.sanitizedStack || '',
} as const;
}
/**
* Given a `errorText` from a tsserver request indicating failure in handling a request,
* prepares a payload for telemetry-logging.
*/
private static parseErrorText(response: Proto.Response) {
const errorText = response.message;
if (errorText) {
const errorPrefix = 'Error processing request. ';
if (errorText.startsWith(errorPrefix)) {
const prefixFreeErrorText = errorText.substr(errorPrefix.length);
const newlineIndex = prefixFreeErrorText.indexOf('\n');
if (newlineIndex >= 0) {
// Newline expected between message and stack.
const stack = prefixFreeErrorText.substring(newlineIndex + 1);
return {
message: prefixFreeErrorText.substring(0, newlineIndex),
stack,
sanitizedStack: TypeScriptServerError.sanitizeStack(stack)
};
}
}
}
return undefined;
}
/**
* Drop everything but ".js" and line/column numbers (though retain "tsserver" if that's the filename).
*/
private static sanitizeStack(message: string | undefined) {
if (!message) {
return '';
}
const regex = /(\btsserver)?(\.(?:ts|tsx|js|jsx)(?::\d+(?::\d+)?)?)\)?$/igm;
let serverStack = '';
while (true) {
const match = regex.exec(message);
if (!match) {
break;
}
// [1] is 'tsserver' or undefined
// [2] is '.js:{line_number}:{column_number}'
serverStack += `${match[1] || 'suppressed'}${match[2]}\n`;
}
return serverStack;
}
}

View File

@@ -0,0 +1,68 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as Proto from '../protocol';
import { TypeScriptServiceConfiguration } from '../utils/configuration';
import { TsServerProcess, TsServerProcessKind } from './server';
declare const Worker: any;
declare type Worker = any;
export class WorkerServerProcess implements TsServerProcess {
public static fork(
tsServerPath: string,
args: readonly string[],
_kind: TsServerProcessKind,
_configuration: TypeScriptServiceConfiguration,
) {
const worker = new Worker(tsServerPath);
return new WorkerServerProcess(worker, [
...args,
// Explicitly give TS Server its path so it can
// load local resources
'--executingFilePath', tsServerPath,
]);
}
private _onDataHandlers = new Set<(data: Proto.Response) => void>();
private _onErrorHandlers = new Set<(err: Error) => void>();
private _onExitHandlers = new Set<(code: number | null) => void>();
public constructor(
private readonly worker: Worker,
args: readonly string[],
) {
worker.addEventListener('message', (msg: any) => {
for (const handler of this._onDataHandlers) {
handler(msg.data);
}
});
worker.postMessage(args);
}
write(serverRequest: Proto.Request): void {
this.worker.postMessage(serverRequest);
}
onData(handler: (response: Proto.Response) => void): void {
this._onDataHandlers.add(handler);
}
onError(handler: (err: Error) => void): void {
this._onErrorHandlers.add(handler);
// Todo: not implemented
}
onExit(handler: (code: number | null) => void): void {
this._onExitHandlers.add(handler);
// Todo: not implemented
}
kill(): void {
this.worker.terminate();
}
}

View File

@@ -0,0 +1,242 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as child_process from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import type { Readable } from 'stream';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import type * as Proto from '../protocol';
import { TypeScriptServiceConfiguration } from '../utils/configuration';
import { Disposable } from '../utils/dispose';
import { TsServerProcess, TsServerProcessKind } from './server';
import { TypeScriptVersionManager } from './versionManager';
const localize = nls.loadMessageBundle();
const defaultSize: number = 8192;
const contentLength: string = 'Content-Length: ';
const contentLengthSize: number = Buffer.byteLength(contentLength, 'utf8');
const blank: number = Buffer.from(' ', 'utf8')[0];
const backslashR: number = Buffer.from('\r', 'utf8')[0];
const backslashN: number = Buffer.from('\n', 'utf8')[0];
class ProtocolBuffer {
private index: number = 0;
private buffer: Buffer = Buffer.allocUnsafe(defaultSize);
public append(data: string | Buffer): void {
let toAppend: Buffer | null = null;
if (Buffer.isBuffer(data)) {
toAppend = data;
} else {
toAppend = Buffer.from(data, 'utf8');
}
if (this.buffer.length - this.index >= toAppend.length) {
toAppend.copy(this.buffer, this.index, 0, toAppend.length);
} else {
let newSize = (Math.ceil((this.index + toAppend.length) / defaultSize) + 1) * defaultSize;
if (this.index === 0) {
this.buffer = Buffer.allocUnsafe(newSize);
toAppend.copy(this.buffer, 0, 0, toAppend.length);
} else {
this.buffer = Buffer.concat([this.buffer.slice(0, this.index), toAppend], newSize);
}
}
this.index += toAppend.length;
}
public tryReadContentLength(): number {
let result = -1;
let current = 0;
// we are utf8 encoding...
while (current < this.index && (this.buffer[current] === blank || this.buffer[current] === backslashR || this.buffer[current] === backslashN)) {
current++;
}
if (this.index < current + contentLengthSize) {
return result;
}
current += contentLengthSize;
let start = current;
while (current < this.index && this.buffer[current] !== backslashR) {
current++;
}
if (current + 3 >= this.index || this.buffer[current + 1] !== backslashN || this.buffer[current + 2] !== backslashR || this.buffer[current + 3] !== backslashN) {
return result;
}
let data = this.buffer.toString('utf8', start, current);
result = parseInt(data);
this.buffer = this.buffer.slice(current + 4);
this.index = this.index - (current + 4);
return result;
}
public tryReadContent(length: number): string | null {
if (this.index < length) {
return null;
}
let result = this.buffer.toString('utf8', 0, length);
let sourceStart = length;
while (sourceStart < this.index && (this.buffer[sourceStart] === backslashR || this.buffer[sourceStart] === backslashN)) {
sourceStart++;
}
this.buffer.copy(this.buffer, 0, sourceStart);
this.index = this.index - sourceStart;
return result;
}
}
class Reader<T> extends Disposable {
private readonly buffer: ProtocolBuffer = new ProtocolBuffer();
private nextMessageLength: number = -1;
public constructor(readable: Readable) {
super();
readable.on('data', data => this.onLengthData(data));
}
private readonly _onError = this._register(new vscode.EventEmitter<Error>());
public readonly onError = this._onError.event;
private readonly _onData = this._register(new vscode.EventEmitter<T>());
public readonly onData = this._onData.event;
private onLengthData(data: Buffer | string): void {
if (this.isDisposed) {
return;
}
try {
this.buffer.append(data);
while (true) {
if (this.nextMessageLength === -1) {
this.nextMessageLength = this.buffer.tryReadContentLength();
if (this.nextMessageLength === -1) {
return;
}
}
const msg = this.buffer.tryReadContent(this.nextMessageLength);
if (msg === null) {
return;
}
this.nextMessageLength = -1;
const json = JSON.parse(msg);
this._onData.fire(json);
}
} catch (e) {
this._onError.fire(e);
}
}
}
export class ChildServerProcess extends Disposable implements TsServerProcess {
private readonly _reader: Reader<Proto.Response>;
public static fork(
tsServerPath: string,
args: readonly string[],
kind: TsServerProcessKind,
configuration: TypeScriptServiceConfiguration,
versionManager: TypeScriptVersionManager,
): ChildServerProcess {
if (!fs.existsSync(tsServerPath)) {
vscode.window.showWarningMessage(localize('noServerFound', 'The path {0} doesn\'t point to a valid tsserver install. Falling back to bundled TypeScript version.', tsServerPath));
versionManager.reset();
tsServerPath = versionManager.currentVersion.tsServerPath;
}
const childProcess = child_process.fork(tsServerPath, args, {
silent: true,
cwd: undefined,
env: this.generatePatchedEnv(process.env, tsServerPath),
execArgv: this.getExecArgv(kind, configuration),
});
return new ChildServerProcess(childProcess);
}
private static generatePatchedEnv(env: any, modulePath: string): any {
const newEnv = Object.assign({}, env);
newEnv['ELECTRON_RUN_AS_NODE'] = '1';
newEnv['NODE_PATH'] = path.join(modulePath, '..', '..', '..');
// Ensure we always have a PATH set
newEnv['PATH'] = newEnv['PATH'] || process.env.PATH;
return newEnv;
}
private static getExecArgv(kind: TsServerProcessKind, configuration: TypeScriptServiceConfiguration): string[] {
const args: string[] = [];
const debugPort = this.getDebugPort(kind);
if (debugPort) {
const inspectFlag = ChildServerProcess.getTssDebugBrk() ? '--inspect-brk' : '--inspect';
args.push(`${inspectFlag}=${debugPort}`);
}
if (configuration.maxTsServerMemory) {
args.push(`--max-old-space-size=${configuration.maxTsServerMemory}`);
}
return args;
}
private static getDebugPort(kind: TsServerProcessKind): number | undefined {
if (kind === TsServerProcessKind.Syntax) {
// We typically only want to debug the main semantic server
return undefined;
}
const value = ChildServerProcess.getTssDebugBrk() || ChildServerProcess.getTssDebug();
if (value) {
const port = parseInt(value);
if (!isNaN(port)) {
return port;
}
}
return undefined;
}
private static getTssDebug(): string | undefined {
return process.env[vscode.env.remoteName ? 'TSS_REMOTE_DEBUG' : 'TSS_DEBUG'];
}
private static getTssDebugBrk(): string | undefined {
return process.env[vscode.env.remoteName ? 'TSS_REMOTE_DEBUG_BRK' : 'TSS_DEBUG_BRK'];
}
private constructor(
private readonly _process: child_process.ChildProcess,
) {
super();
this._reader = this._register(new Reader<Proto.Response>(this._process.stdout!));
}
write(serverRequest: Proto.Request): void {
this._process.stdin!.write(JSON.stringify(serverRequest) + '\r\n', 'utf8');
}
onData(handler: (data: Proto.Response) => void): void {
this._reader.onData(handler);
}
onExit(handler: (code: number | null) => void): void {
this._process.on('exit', handler);
}
onError(handler: (err: Error) => void): void {
this._process.on('error', handler);
this._reader.onError(handler);
}
kill(): void {
this._process.kill();
this._reader.dispose();
}
}

View File

@@ -0,0 +1,267 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as vscode from 'vscode';
import { OngoingRequestCancellerFactory } from '../tsServer/cancellation';
import { ClientCapabilities, ClientCapability, ServerType } from '../typescriptService';
import API from '../utils/api';
import { SeparateSyntaxServerConfiguration, TsServerLogLevel, TypeScriptServiceConfiguration } from '../utils/configuration';
import { Logger } from '../utils/logger';
import { isWeb } from '../utils/platform';
import { TypeScriptPluginPathsProvider } from '../utils/pluginPathsProvider';
import { PluginManager } from '../utils/plugins';
import { TelemetryReporter } from '../utils/telemetry';
import Tracer from '../utils/tracer';
import { ILogDirectoryProvider } from './logDirectoryProvider';
import { GetErrRoutingTsServer, ITypeScriptServer, ProcessBasedTsServer, SyntaxRoutingTsServer, TsServerDelegate, TsServerProcessFactory, TsServerProcessKind } from './server';
import { TypeScriptVersionManager } from './versionManager';
import { ITypeScriptVersionProvider, TypeScriptVersion } from './versionProvider';
const enum CompositeServerType {
/** Run a single server that handles all commands */
Single,
/** Run a separate server for syntax commands */
SeparateSyntax,
/** Use a separate syntax server while the project is loading */
DynamicSeparateSyntax,
/** Only enable the syntax server */
SyntaxOnly
}
export class TypeScriptServerSpawner {
public constructor(
private readonly _versionProvider: ITypeScriptVersionProvider,
private readonly _versionManager: TypeScriptVersionManager,
private readonly _logDirectoryProvider: ILogDirectoryProvider,
private readonly _pluginPathsProvider: TypeScriptPluginPathsProvider,
private readonly _logger: Logger,
private readonly _telemetryReporter: TelemetryReporter,
private readonly _tracer: Tracer,
private readonly _factory: TsServerProcessFactory,
) { }
public spawn(
version: TypeScriptVersion,
capabilities: ClientCapabilities,
configuration: TypeScriptServiceConfiguration,
pluginManager: PluginManager,
cancellerFactory: OngoingRequestCancellerFactory,
delegate: TsServerDelegate,
): ITypeScriptServer {
let primaryServer: ITypeScriptServer;
const serverType = this.getCompositeServerType(version, capabilities, configuration);
switch (serverType) {
case CompositeServerType.SeparateSyntax:
case CompositeServerType.DynamicSeparateSyntax:
{
const enableDynamicRouting = serverType === CompositeServerType.DynamicSeparateSyntax;
primaryServer = new SyntaxRoutingTsServer({
syntax: this.spawnTsServer(TsServerProcessKind.Syntax, version, configuration, pluginManager, cancellerFactory),
semantic: this.spawnTsServer(TsServerProcessKind.Semantic, version, configuration, pluginManager, cancellerFactory),
}, delegate, enableDynamicRouting);
break;
}
case CompositeServerType.Single:
{
primaryServer = this.spawnTsServer(TsServerProcessKind.Main, version, configuration, pluginManager, cancellerFactory);
break;
}
case CompositeServerType.SyntaxOnly:
{
primaryServer = this.spawnTsServer(TsServerProcessKind.Syntax, version, configuration, pluginManager, cancellerFactory);
break;
}
}
if (this.shouldUseSeparateDiagnosticsServer(configuration)) {
return new GetErrRoutingTsServer({
getErr: this.spawnTsServer(TsServerProcessKind.Diagnostics, version, configuration, pluginManager, cancellerFactory),
primary: primaryServer,
}, delegate);
}
return primaryServer;
}
private getCompositeServerType(
version: TypeScriptVersion,
capabilities: ClientCapabilities,
configuration: TypeScriptServiceConfiguration,
): CompositeServerType {
if (!capabilities.has(ClientCapability.Semantic)) {
return CompositeServerType.SyntaxOnly;
}
switch (configuration.separateSyntaxServer) {
case SeparateSyntaxServerConfiguration.Disabled:
return CompositeServerType.Single;
case SeparateSyntaxServerConfiguration.Enabled:
if (version.apiVersion?.gte(API.v340)) {
return version.apiVersion?.gte(API.v400)
? CompositeServerType.DynamicSeparateSyntax
: CompositeServerType.SeparateSyntax;
}
return CompositeServerType.Single;
}
}
private shouldUseSeparateDiagnosticsServer(
configuration: TypeScriptServiceConfiguration,
): boolean {
return configuration.enableProjectDiagnostics;
}
private spawnTsServer(
kind: TsServerProcessKind,
version: TypeScriptVersion,
configuration: TypeScriptServiceConfiguration,
pluginManager: PluginManager,
cancellerFactory: OngoingRequestCancellerFactory,
): ITypeScriptServer {
const apiVersion = version.apiVersion || API.defaultVersion;
const canceller = cancellerFactory.create(kind, this._tracer);
const { args, tsServerLogFile } = this.getTsServerArgs(kind, configuration, version, apiVersion, pluginManager, canceller.cancellationPipeName);
if (TypeScriptServerSpawner.isLoggingEnabled(configuration)) {
if (tsServerLogFile) {
this._logger.info(`<${kind}> Log file: ${tsServerLogFile}`);
} else {
this._logger.error(`<${kind}> Could not create log directory`);
}
}
this._logger.info(`<${kind}> Forking...`);
const process = this._factory.fork(version.tsServerPath, args, kind, configuration, this._versionManager);
this._logger.info(`<${kind}> Starting...`);
return new ProcessBasedTsServer(
kind,
this.kindToServerType(kind),
process!,
tsServerLogFile,
canceller,
version,
this._telemetryReporter,
this._tracer);
}
private kindToServerType(kind: TsServerProcessKind): ServerType {
switch (kind) {
case TsServerProcessKind.Syntax:
return ServerType.Syntax;
case TsServerProcessKind.Main:
case TsServerProcessKind.Semantic:
case TsServerProcessKind.Diagnostics:
default:
return ServerType.Semantic;
}
}
private getTsServerArgs(
kind: TsServerProcessKind,
configuration: TypeScriptServiceConfiguration,
currentVersion: TypeScriptVersion,
apiVersion: API,
pluginManager: PluginManager,
cancellationPipeName: string | undefined,
): { args: string[], tsServerLogFile: string | undefined } {
const args: string[] = [];
let tsServerLogFile: string | undefined;
if (kind === TsServerProcessKind.Syntax) {
if (apiVersion.gte(API.v401)) {
args.push('--serverMode', 'partialSemantic');
} else {
args.push('--syntaxOnly');
}
}
if (apiVersion.gte(API.v250)) {
args.push('--useInferredProjectPerProjectRoot');
} else {
args.push('--useSingleInferredProject');
}
if (configuration.disableAutomaticTypeAcquisition || kind === TsServerProcessKind.Syntax || kind === TsServerProcessKind.Diagnostics) {
args.push('--disableAutomaticTypingAcquisition');
}
if (kind === TsServerProcessKind.Semantic || kind === TsServerProcessKind.Main) {
args.push('--enableTelemetry');
}
if (cancellationPipeName) {
args.push('--cancellationPipeName', cancellationPipeName + '*');
}
if (TypeScriptServerSpawner.isLoggingEnabled(configuration)) {
if (isWeb()) {
args.push('--logVerbosity', TsServerLogLevel.toString(configuration.tsServerLogLevel));
} else {
const logDir = this._logDirectoryProvider.getNewLogDirectory();
if (logDir) {
tsServerLogFile = path.join(logDir, `tsserver.log`);
args.push('--logVerbosity', TsServerLogLevel.toString(configuration.tsServerLogLevel));
args.push('--logFile', tsServerLogFile);
}
}
}
if (!isWeb()) {
const pluginPaths = this._pluginPathsProvider.getPluginPaths();
if (pluginManager.plugins.length) {
args.push('--globalPlugins', pluginManager.plugins.map(x => x.name).join(','));
const isUsingBundledTypeScriptVersion = currentVersion.path === this._versionProvider.defaultVersion.path;
for (const plugin of pluginManager.plugins) {
if (isUsingBundledTypeScriptVersion || plugin.enableForWorkspaceTypeScriptVersions) {
pluginPaths.push(plugin.path);
}
}
}
if (pluginPaths.length !== 0) {
args.push('--pluginProbeLocations', pluginPaths.join(','));
}
}
if (configuration.npmLocation) {
args.push('--npmLocation', `"${configuration.npmLocation}"`);
}
if (apiVersion.gte(API.v260)) {
args.push('--locale', TypeScriptServerSpawner.getTsLocale(configuration));
}
if (apiVersion.gte(API.v291)) {
args.push('--noGetErrOnBackgroundUpdate');
}
if (apiVersion.gte(API.v345)) {
args.push('--validateDefaultNpmLocation');
}
return { args, tsServerLogFile };
}
private static isLoggingEnabled(configuration: TypeScriptServiceConfiguration) {
return configuration.tsServerLogLevel !== TsServerLogLevel.Off;
}
private static getTsLocale(configuration: TypeScriptServiceConfiguration): string {
return configuration.locale
? configuration.locale
: vscode.env.language;
}
}

View File

@@ -0,0 +1,176 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { TypeScriptServiceConfiguration } from '../utils/configuration';
import { Disposable } from '../utils/dispose';
import { ITypeScriptVersionProvider, TypeScriptVersion } from './versionProvider';
const localize = nls.loadMessageBundle();
const useWorkspaceTsdkStorageKey = 'typescript.useWorkspaceTsdk';
const suppressPromptWorkspaceTsdkStorageKey = 'typescript.suppressPromptWorkspaceTsdk';
interface QuickPickItem extends vscode.QuickPickItem {
run(): void;
}
export class TypeScriptVersionManager extends Disposable {
private _currentVersion: TypeScriptVersion;
public constructor(
private configuration: TypeScriptServiceConfiguration,
private readonly versionProvider: ITypeScriptVersionProvider,
private readonly workspaceState: vscode.Memento
) {
super();
this._currentVersion = this.versionProvider.defaultVersion;
if (this.useWorkspaceTsdkSetting) {
const localVersion = this.versionProvider.localVersion;
if (localVersion) {
this._currentVersion = localVersion;
}
}
if (this.isInPromptWorkspaceTsdkState(configuration)) {
setImmediate(() => {
this.promptUseWorkspaceTsdk();
});
}
}
private readonly _onDidPickNewVersion = this._register(new vscode.EventEmitter<void>());
public readonly onDidPickNewVersion = this._onDidPickNewVersion.event;
public updateConfiguration(nextConfiguration: TypeScriptServiceConfiguration) {
const lastConfiguration = this.configuration;
this.configuration = nextConfiguration;
if (
!this.isInPromptWorkspaceTsdkState(lastConfiguration)
&& this.isInPromptWorkspaceTsdkState(nextConfiguration)
) {
this.promptUseWorkspaceTsdk();
}
}
public get currentVersion(): TypeScriptVersion {
return this._currentVersion;
}
public reset(): void {
this._currentVersion = this.versionProvider.bundledVersion;
}
public async promptUserForVersion(): Promise<void> {
const selected = await vscode.window.showQuickPick<QuickPickItem>([
this.getBundledPickItem(),
...this.getLocalPickItems(),
LearnMorePickItem,
], {
placeHolder: localize(
'selectTsVersion',
"Select the TypeScript version used for JavaScript and TypeScript language features"),
});
return selected?.run();
}
private getBundledPickItem(): QuickPickItem {
const bundledVersion = this.versionProvider.defaultVersion;
return {
label: (!this.useWorkspaceTsdkSetting
? '• '
: '') + localize('useVSCodeVersionOption', "Use VS Code's Version"),
description: bundledVersion.displayName,
detail: bundledVersion.pathLabel,
run: async () => {
await this.workspaceState.update(useWorkspaceTsdkStorageKey, false);
this.updateActiveVersion(bundledVersion);
},
};
}
private getLocalPickItems(): QuickPickItem[] {
return this.versionProvider.localVersions.map(version => {
return {
label: (this.useWorkspaceTsdkSetting && this.currentVersion.eq(version)
? '• '
: '') + localize('useWorkspaceVersionOption', "Use Workspace Version"),
description: version.displayName,
detail: version.pathLabel,
run: async () => {
await this.workspaceState.update(useWorkspaceTsdkStorageKey, true);
const tsConfig = vscode.workspace.getConfiguration('typescript');
await tsConfig.update('tsdk', version.pathLabel, false);
this.updateActiveVersion(version);
},
};
});
}
private async promptUseWorkspaceTsdk(): Promise<void> {
const workspaceVersion = this.versionProvider.localVersion;
if (workspaceVersion === undefined) {
throw new Error('Could not prompt to use workspace TypeScript version because no workspace version is specified');
}
const allowIt = localize('allow', 'Allow');
const dismissPrompt = localize('dismiss', 'Dismiss');
const suppressPrompt = localize('suppress prompt', 'Never in this Workspace');
const result = await vscode.window.showInformationMessage(localize('promptUseWorkspaceTsdk', 'This workspace contains a TypeScript version. Would you like to use the workspace TypeScript version for TypeScript and JavaScript language features?'),
allowIt,
dismissPrompt,
suppressPrompt
);
if (result === allowIt) {
await this.workspaceState.update(useWorkspaceTsdkStorageKey, true);
this.updateActiveVersion(workspaceVersion);
} else if (result === suppressPrompt) {
await this.workspaceState.update(suppressPromptWorkspaceTsdkStorageKey, true);
}
}
private updateActiveVersion(pickedVersion: TypeScriptVersion) {
const oldVersion = this.currentVersion;
this._currentVersion = pickedVersion;
if (!oldVersion.eq(pickedVersion)) {
this._onDidPickNewVersion.fire();
}
}
private get useWorkspaceTsdkSetting(): boolean {
return this.workspaceState.get<boolean>(useWorkspaceTsdkStorageKey, false);
}
private get suppressPromptWorkspaceTsdkSetting(): boolean {
return this.workspaceState.get<boolean>(suppressPromptWorkspaceTsdkStorageKey, false);
}
private isInPromptWorkspaceTsdkState(configuration: TypeScriptServiceConfiguration) {
return (
configuration.localTsdk !== null
&& configuration.enablePromptUseWorkspaceTsdk === true
&& this.suppressPromptWorkspaceTsdkSetting === false
&& this.useWorkspaceTsdkSetting === false
);
}
}
const LearnMorePickItem: QuickPickItem = {
label: localize('learnMore', 'Learn more about managing TypeScript versions'),
description: '',
run: () => {
vscode.env.openExternal(vscode.Uri.parse('https://go.microsoft.com/fwlink/?linkid=839919'));
}
};

View File

@@ -0,0 +1,198 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
import API from '../utils/api';
import { TypeScriptServiceConfiguration } from '../utils/configuration';
import { RelativeWorkspacePathResolver } from '../utils/relativePathResolver';
import { ITypeScriptVersionProvider, localize, TypeScriptVersion, TypeScriptVersionSource } from './versionProvider';
export class DiskTypeScriptVersionProvider implements ITypeScriptVersionProvider {
public constructor(
private configuration?: TypeScriptServiceConfiguration
) { }
public updateConfiguration(configuration: TypeScriptServiceConfiguration): void {
this.configuration = configuration;
}
public get defaultVersion(): TypeScriptVersion {
return this.globalVersion || this.bundledVersion;
}
public get globalVersion(): TypeScriptVersion | undefined {
if (this.configuration?.globalTsdk) {
const globals = this.loadVersionsFromSetting(TypeScriptVersionSource.UserSetting, this.configuration.globalTsdk);
if (globals && globals.length) {
return globals[0];
}
}
return this.contributedTsNextVersion;
}
public get localVersion(): TypeScriptVersion | undefined {
const tsdkVersions = this.localTsdkVersions;
if (tsdkVersions && tsdkVersions.length) {
return tsdkVersions[0];
}
const nodeVersions = this.localNodeModulesVersions;
if (nodeVersions && nodeVersions.length === 1) {
return nodeVersions[0];
}
return undefined;
}
public get localVersions(): TypeScriptVersion[] {
const allVersions = this.localTsdkVersions.concat(this.localNodeModulesVersions);
const paths = new Set<string>();
return allVersions.filter(x => {
if (paths.has(x.path)) {
return false;
}
paths.add(x.path);
return true;
});
}
public get bundledVersion(): TypeScriptVersion {
const version = this.getContributedVersion(TypeScriptVersionSource.Bundled, 'vscode.typescript-language-features', ['..', 'node_modules']);
if (version) {
return version;
}
vscode.window.showErrorMessage(localize(
'noBundledServerFound',
'VS Code\'s tsserver was deleted by another application such as a misbehaving virus detection tool. Please reinstall VS Code.'));
throw new Error('Could not find bundled tsserver.js');
}
private get contributedTsNextVersion(): TypeScriptVersion | undefined {
return this.getContributedVersion(TypeScriptVersionSource.TsNightlyExtension, 'ms-vscode.vscode-typescript-next', ['node_modules']);
}
private getContributedVersion(source: TypeScriptVersionSource, extensionId: string, pathToTs: readonly string[]): TypeScriptVersion | undefined {
try {
const extension = vscode.extensions.getExtension(extensionId);
if (extension) {
const serverPath = path.join(extension.extensionPath, ...pathToTs, 'typescript', 'lib', 'tsserver.js');
const bundledVersion = new TypeScriptVersion(source, serverPath, DiskTypeScriptVersionProvider.getApiVersion(serverPath), '');
if (bundledVersion.isValid) {
return bundledVersion;
}
}
} catch {
// noop
}
return undefined;
}
private get localTsdkVersions(): TypeScriptVersion[] {
const localTsdk = this.configuration?.localTsdk;
return localTsdk ? this.loadVersionsFromSetting(TypeScriptVersionSource.WorkspaceSetting, localTsdk) : [];
}
private loadVersionsFromSetting(source: TypeScriptVersionSource, tsdkPathSetting: string): TypeScriptVersion[] {
if (path.isAbsolute(tsdkPathSetting)) {
const serverPath = path.join(tsdkPathSetting, 'tsserver.js');
return [
new TypeScriptVersion(source,
serverPath,
DiskTypeScriptVersionProvider.getApiVersion(serverPath),
tsdkPathSetting)
];
}
const workspacePath = RelativeWorkspacePathResolver.asAbsoluteWorkspacePath(tsdkPathSetting);
if (workspacePath !== undefined) {
const serverPath = path.join(workspacePath, 'tsserver.js');
return [
new TypeScriptVersion(source,
serverPath,
DiskTypeScriptVersionProvider.getApiVersion(serverPath),
tsdkPathSetting)
];
}
return this.loadTypeScriptVersionsFromPath(source, tsdkPathSetting);
}
private get localNodeModulesVersions(): TypeScriptVersion[] {
return this.loadTypeScriptVersionsFromPath(TypeScriptVersionSource.NodeModules, path.join('node_modules', 'typescript', 'lib'))
.filter(x => x.isValid);
}
private loadTypeScriptVersionsFromPath(source: TypeScriptVersionSource, relativePath: string): TypeScriptVersion[] {
if (!vscode.workspace.workspaceFolders) {
return [];
}
const versions: TypeScriptVersion[] = [];
for (const root of vscode.workspace.workspaceFolders) {
let label: string = relativePath;
if (vscode.workspace.workspaceFolders.length > 1) {
label = path.join(root.name, relativePath);
}
const serverPath = path.join(root.uri.fsPath, relativePath, 'tsserver.js');
versions.push(new TypeScriptVersion(source, serverPath, DiskTypeScriptVersionProvider.getApiVersion(serverPath), label));
}
return versions;
}
private static getApiVersion(serverPath: string): API | undefined {
const version = DiskTypeScriptVersionProvider.getTypeScriptVersion(serverPath);
if (version) {
return version;
}
// Allow TS developers to provide custom version
const tsdkVersion = vscode.workspace.getConfiguration().get<string | undefined>('typescript.tsdk_version', undefined);
if (tsdkVersion) {
return API.fromVersionString(tsdkVersion);
}
return undefined;
}
private static getTypeScriptVersion(serverPath: string): API | undefined {
if (!fs.existsSync(serverPath)) {
return undefined;
}
const p = serverPath.split(path.sep);
if (p.length <= 2) {
return undefined;
}
const p2 = p.slice(0, -2);
const modulePath = p2.join(path.sep);
let fileName = path.join(modulePath, 'package.json');
if (!fs.existsSync(fileName)) {
// Special case for ts dev versions
if (path.basename(modulePath) === 'built') {
fileName = path.join(modulePath, '..', 'package.json');
}
}
if (!fs.existsSync(fileName)) {
return undefined;
}
const contents = fs.readFileSync(fileName).toString();
let desc: any = null;
try {
desc = JSON.parse(contents);
} catch (err) {
return undefined;
}
if (!desc || !desc.version) {
return undefined;
}
return desc.version ? API.fromVersionString(desc.version) : undefined;
}
}

View File

@@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
import API from '../utils/api';
import { TypeScriptServiceConfiguration } from '../utils/configuration';
export const localize = nls.loadMessageBundle();
export const enum TypeScriptVersionSource {
Bundled = 'bundled',
TsNightlyExtension = 'ts-nightly-extension',
NodeModules = 'node-modules',
UserSetting = 'user-setting',
WorkspaceSetting = 'workspace-setting',
}
export class TypeScriptVersion {
constructor(
public readonly source: TypeScriptVersionSource,
public readonly path: string,
public readonly apiVersion: API | undefined,
private readonly _pathLabel?: string,
) { }
public get tsServerPath(): string {
return this.path;
}
public get pathLabel(): string {
return this._pathLabel ?? this.path;
}
public get isValid(): boolean {
return this.apiVersion !== undefined;
}
public eq(other: TypeScriptVersion): boolean {
if (this.path !== other.path) {
return false;
}
if (this.apiVersion === other.apiVersion) {
return true;
}
if (!this.apiVersion || !other.apiVersion) {
return false;
}
return this.apiVersion.eq(other.apiVersion);
}
public get displayName(): string {
const version = this.apiVersion;
return version ? version.displayName : localize(
'couldNotLoadTsVersion', 'Could not load the TypeScript version at this path');
}
}
export interface ITypeScriptVersionProvider {
updateConfiguration(configuration: TypeScriptServiceConfiguration): void;
readonly defaultVersion: TypeScriptVersion;
readonly globalVersion: TypeScriptVersion | undefined;
readonly localVersion: TypeScriptVersion | undefined;
readonly localVersions: readonly TypeScriptVersion[];
readonly bundledVersion: TypeScriptVersion;
}

View File

@@ -0,0 +1,220 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { Command, CommandManager } from '../commands/commandManager';
import { ITypeScriptServiceClient } from '../typescriptService';
import { coalesce } from '../utils/arrays';
import { Disposable } from '../utils/dispose';
import { isTypeScriptDocument } from '../utils/languageModeIds';
import { isImplicitProjectConfigFile, openOrCreateConfig, openProjectConfigForFile, openProjectConfigOrPromptToCreate, ProjectType } from '../utils/tsconfig';
import { TypeScriptVersion } from './versionProvider';
const localize = nls.loadMessageBundle();
namespace ProjectInfoState {
export const enum Type { None, Pending, Resolved }
export const None = Object.freeze({ type: Type.None } as const);
export class Pending {
public readonly type = Type.Pending;
public readonly cancellation = new vscode.CancellationTokenSource();
constructor(
public readonly resource: vscode.Uri,
) { }
}
export class Resolved {
public readonly type = Type.Resolved;
constructor(
public readonly resource: vscode.Uri,
public readonly configFile: string,
) { }
}
export type State = typeof None | Pending | Resolved;
}
interface QuickPickItem extends vscode.QuickPickItem {
run(): void;
}
class ProjectStatusCommand implements Command {
public readonly id = '_typescript.projectStatus';
public constructor(
private readonly _client: ITypeScriptServiceClient,
private readonly _delegate: () => ProjectInfoState.State,
) { }
public async execute(): Promise<void> {
const info = this._delegate();
const result = await vscode.window.showQuickPick<QuickPickItem>(coalesce([
this.getProjectItem(info),
this.getVersionItem(),
this.getHelpItem(),
]), {
placeHolder: localize('projectQuickPick.placeholder', "TypeScript Project Info"),
});
return result?.run();
}
private getVersionItem(): QuickPickItem {
return {
label: localize('projectQuickPick.version.label', "Select TypeScript Version..."),
description: this._client.apiVersion.displayName,
run: () => {
this._client.showVersionPicker();
}
};
}
private getProjectItem(info: ProjectInfoState.State): QuickPickItem | undefined {
const rootPath = info.type === ProjectInfoState.Type.Resolved ? this._client.getWorkspaceRootForResource(info.resource) : undefined;
if (!rootPath) {
return undefined;
}
if (info.type === ProjectInfoState.Type.Resolved) {
if (isImplicitProjectConfigFile(info.configFile)) {
return {
label: localize('projectQuickPick.project.create', "Create tsconfig"),
detail: localize('projectQuickPick.project.create.description', "This file is currently not part of a tsconfig/jsconfig project"),
run: () => {
openOrCreateConfig(ProjectType.TypeScript, rootPath, this._client.configuration);
}
};
}
}
return {
label: localize('projectQuickPick.version.goProjectConfig', "Open tsconfig"),
description: info.type === ProjectInfoState.Type.Resolved ? vscode.workspace.asRelativePath(info.configFile) : undefined,
run: () => {
if (info.type === ProjectInfoState.Type.Resolved) {
openProjectConfigOrPromptToCreate(ProjectType.TypeScript, this._client, rootPath, info.configFile);
} else if (info.type === ProjectInfoState.Type.Pending) {
openProjectConfigForFile(ProjectType.TypeScript, this._client, info.resource);
}
}
};
}
private getHelpItem(): QuickPickItem {
return {
label: localize('projectQuickPick.help', "TypeScript help"),
run: () => {
vscode.env.openExternal(vscode.Uri.parse('https://go.microsoft.com/fwlink/?linkid=839919')); // TODO:
}
};
}
}
export default class VersionStatus extends Disposable {
private readonly _statusBarEntry: vscode.StatusBarItem;
private _ready = false;
private _state: ProjectInfoState.State = ProjectInfoState.None;
constructor(
private readonly _client: ITypeScriptServiceClient,
commandManager: CommandManager,
) {
super();
this._statusBarEntry = this._register(vscode.window.createStatusBarItem({
id: 'status.typescript',
name: localize('projectInfo.name', "TypeScript: Project Info"),
alignment: vscode.StatusBarAlignment.Right,
priority: 99 /* to the right of editor status (100) */
}));
const command = new ProjectStatusCommand(this._client, () => this._state);
commandManager.register(command);
this._statusBarEntry.command = command.id;
vscode.window.onDidChangeActiveTextEditor(this.updateStatus, this, this._disposables);
this._client.onReady(() => {
this._ready = true;
this.updateStatus();
});
this._register(this._client.onTsServerStarted(({ version }) => this.onDidChangeTypeScriptVersion(version)));
}
private onDidChangeTypeScriptVersion(version: TypeScriptVersion) {
this._statusBarEntry.text = version.displayName;
this._statusBarEntry.tooltip = version.path;
this.updateStatus();
}
private async updateStatus() {
if (!vscode.window.activeTextEditor) {
this.hide();
return;
}
const doc = vscode.window.activeTextEditor.document;
if (isTypeScriptDocument(doc)) {
const file = this._client.normalizedPath(doc.uri);
if (file) {
this._statusBarEntry.show();
if (!this._ready) {
return;
}
const pendingState = new ProjectInfoState.Pending(doc.uri);
this.updateState(pendingState);
const response = await this._client.execute('projectInfo', { file, needFileNameList: false }, pendingState.cancellation.token);
if (response.type === 'response' && response.body) {
if (this._state === pendingState) {
this.updateState(new ProjectInfoState.Resolved(doc.uri, response.body.configFileName));
this._statusBarEntry.show();
}
}
return;
}
}
if (!vscode.window.activeTextEditor.viewColumn) {
// viewColumn is undefined for the debug/output panel, but we still want
// to show the version info in the existing editor
return;
}
this.hide();
}
private hide(): void {
this._statusBarEntry.hide();
this.updateState(ProjectInfoState.None);
}
private updateState(newState: ProjectInfoState.State): void {
if (this._state === newState) {
return;
}
if (this._state.type === ProjectInfoState.Type.Pending) {
this._state.cancellation.cancel();
this._state.cancellation.dispose();
}
this._state = newState;
}
}

View File

@@ -0,0 +1,318 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* --------------------------------------------------------------------------------------------
* Includes code from typescript-sublime-plugin project, obtained from
* https://github.com/microsoft/TypeScript-Sublime-Plugin/blob/master/TypeScript%20Indent.tmPreferences
* ------------------------------------------------------------------------------------------ */
import * as vscode from 'vscode';
import { DiagnosticKind } from './languageFeatures/diagnostics';
import FileConfigurationManager from './languageFeatures/fileConfigurationManager';
import LanguageProvider from './languageProvider';
import * as Proto from './protocol';
import * as PConst from './protocol.const';
import { OngoingRequestCancellerFactory } from './tsServer/cancellation';
import { ILogDirectoryProvider } from './tsServer/logDirectoryProvider';
import { TsServerProcessFactory } from './tsServer/server';
import { ITypeScriptVersionProvider } from './tsServer/versionProvider';
import VersionStatus from './tsServer/versionStatus';
import TypeScriptServiceClient from './typescriptServiceClient';
import { coalesce, flatten } from './utils/arrays';
import { CommandManager } from './commands/commandManager';
import { Disposable } from './utils/dispose';
import * as errorCodes from './utils/errorCodes';
import { DiagnosticLanguage, LanguageDescription } from './utils/languageDescription';
import { PluginManager } from './utils/plugins';
import * as typeConverters from './utils/typeConverters';
import TypingsStatus, { AtaProgressReporter } from './utils/typingsStatus';
import * as ProjectStatus from './utils/largeProjectStatus';
namespace Experimental {
export interface Diagnostic extends Proto.Diagnostic {
readonly reportsDeprecated?: {}
}
}
// Style check diagnostics that can be reported as warnings
const styleCheckDiagnostics = new Set([
...errorCodes.variableDeclaredButNeverUsed,
...errorCodes.propertyDeclaretedButNeverUsed,
...errorCodes.allImportsAreUnused,
...errorCodes.unreachableCode,
...errorCodes.unusedLabel,
...errorCodes.fallThroughCaseInSwitch,
...errorCodes.notAllCodePathsReturnAValue,
]);
export default class TypeScriptServiceClientHost extends Disposable {
private readonly client: TypeScriptServiceClient;
private readonly languages: LanguageProvider[] = [];
private readonly languagePerId = new Map<string, LanguageProvider>();
private readonly typingsStatus: TypingsStatus;
private readonly fileConfigurationManager: FileConfigurationManager;
private reportStyleCheckAsWarnings: boolean = true;
private readonly commandManager: CommandManager;
constructor(
descriptions: LanguageDescription[],
workspaceState: vscode.Memento,
onCaseInsenitiveFileSystem: boolean,
services: {
pluginManager: PluginManager,
commandManager: CommandManager,
logDirectoryProvider: ILogDirectoryProvider,
cancellerFactory: OngoingRequestCancellerFactory,
versionProvider: ITypeScriptVersionProvider,
processFactory: TsServerProcessFactory,
},
onCompletionAccepted: (item: vscode.CompletionItem) => void,
) {
super();
this.commandManager = services.commandManager;
const allModeIds = this.getAllModeIds(descriptions, services.pluginManager);
this.client = this._register(new TypeScriptServiceClient(
workspaceState,
onCaseInsenitiveFileSystem,
services,
allModeIds));
this.client.onDiagnosticsReceived(({ kind, resource, diagnostics }) => {
this.diagnosticsReceived(kind, resource, diagnostics);
}, null, this._disposables);
this.client.onConfigDiagnosticsReceived(diag => this.configFileDiagnosticsReceived(diag), null, this._disposables);
this.client.onResendModelsRequested(() => this.populateService(), null, this._disposables);
this._register(new VersionStatus(this.client, services.commandManager));
this._register(new AtaProgressReporter(this.client));
this.typingsStatus = this._register(new TypingsStatus(this.client));
this._register(ProjectStatus.create(this.client));
this.fileConfigurationManager = this._register(new FileConfigurationManager(this.client, onCaseInsenitiveFileSystem));
for (const description of descriptions) {
const manager = new LanguageProvider(this.client, description, this.commandManager, this.client.telemetryReporter, this.typingsStatus, this.fileConfigurationManager, onCompletionAccepted);
this.languages.push(manager);
this._register(manager);
this.languagePerId.set(description.id, manager);
}
import('./languageFeatures/updatePathsOnRename').then(module =>
this._register(module.register(this.client, this.fileConfigurationManager, uri => this.handles(uri))));
import('./languageFeatures/workspaceSymbols').then(module =>
this._register(module.register(this.client, allModeIds)));
this.client.ensureServiceStarted();
this.client.onReady(() => {
const languages = new Set<string>();
for (const plugin of services.pluginManager.plugins) {
if (plugin.configNamespace && plugin.languages.length) {
this.registerExtensionLanguageProvider({
id: plugin.configNamespace,
modeIds: Array.from(plugin.languages),
diagnosticSource: 'ts-plugin',
diagnosticLanguage: DiagnosticLanguage.TypeScript,
diagnosticOwner: 'typescript',
isExternal: true
}, onCompletionAccepted);
} else {
for (const language of plugin.languages) {
languages.add(language);
}
}
}
if (languages.size) {
this.registerExtensionLanguageProvider({
id: 'typescript-plugins',
modeIds: Array.from(languages.values()),
diagnosticSource: 'ts-plugin',
diagnosticLanguage: DiagnosticLanguage.TypeScript,
diagnosticOwner: 'typescript',
isExternal: true
}, onCompletionAccepted);
}
});
this.client.onTsServerStarted(() => {
this.triggerAllDiagnostics();
});
vscode.workspace.onDidChangeConfiguration(this.configurationChanged, this, this._disposables);
this.configurationChanged();
}
private registerExtensionLanguageProvider(description: LanguageDescription, onCompletionAccepted: (item: vscode.CompletionItem) => void) {
const manager = new LanguageProvider(this.client, description, this.commandManager, this.client.telemetryReporter, this.typingsStatus, this.fileConfigurationManager, onCompletionAccepted);
this.languages.push(manager);
this._register(manager);
this.languagePerId.set(description.id, manager);
}
private getAllModeIds(descriptions: LanguageDescription[], pluginManager: PluginManager) {
const allModeIds = flatten([
...descriptions.map(x => x.modeIds),
...pluginManager.plugins.map(x => x.languages)
]);
return allModeIds;
}
public get serviceClient(): TypeScriptServiceClient {
return this.client;
}
public reloadProjects(): void {
this.client.executeWithoutWaitingForResponse('reloadProjects', null);
this.triggerAllDiagnostics();
}
public async handles(resource: vscode.Uri): Promise<boolean> {
const provider = await this.findLanguage(resource);
if (provider) {
return true;
}
return this.client.bufferSyncSupport.handles(resource);
}
private configurationChanged(): void {
const typescriptConfig = vscode.workspace.getConfiguration('typescript');
this.reportStyleCheckAsWarnings = typescriptConfig.get('reportStyleChecksAsWarnings', true);
}
private async findLanguage(resource: vscode.Uri): Promise<LanguageProvider | undefined> {
try {
const doc = await vscode.workspace.openTextDocument(resource);
return this.languages.find(language => language.handles(resource, doc));
} catch {
return undefined;
}
}
private triggerAllDiagnostics() {
for (const language of this.languagePerId.values()) {
language.triggerAllDiagnostics();
}
}
private populateService(): void {
this.fileConfigurationManager.reset();
for (const language of this.languagePerId.values()) {
language.reInitialize();
}
}
private async diagnosticsReceived(
kind: DiagnosticKind,
resource: vscode.Uri,
diagnostics: Proto.Diagnostic[]
): Promise<void> {
const language = await this.findLanguage(resource);
if (language) {
language.diagnosticsReceived(
kind,
resource,
this.createMarkerDatas(diagnostics, language.diagnosticSource));
}
}
private configFileDiagnosticsReceived(event: Proto.ConfigFileDiagnosticEvent): void {
// See https://github.com/microsoft/TypeScript/issues/10384
const body = event.body;
if (!body || !body.diagnostics || !body.configFile) {
return;
}
this.findLanguage(this.client.toResource(body.configFile)).then(language => {
if (!language) {
return;
}
language.configFileDiagnosticsReceived(this.client.toResource(body.configFile), body.diagnostics.map(tsDiag => {
const range = tsDiag.start && tsDiag.end ? typeConverters.Range.fromTextSpan(tsDiag) : new vscode.Range(0, 0, 0, 1);
const diagnostic = new vscode.Diagnostic(range, body.diagnostics[0].text, this.getDiagnosticSeverity(tsDiag));
diagnostic.source = language.diagnosticSource;
return diagnostic;
}));
});
}
private createMarkerDatas(
diagnostics: Proto.Diagnostic[],
source: string
): (vscode.Diagnostic & { reportUnnecessary: any, reportDeprecated: any })[] {
return diagnostics.map(tsDiag => this.tsDiagnosticToVsDiagnostic(tsDiag, source));
}
private tsDiagnosticToVsDiagnostic(diagnostic: Experimental.Diagnostic, source: string): vscode.Diagnostic & { reportUnnecessary: any, reportDeprecated: any } {
const { start, end, text } = diagnostic;
const range = new vscode.Range(typeConverters.Position.fromLocation(start), typeConverters.Position.fromLocation(end));
const converted = new vscode.Diagnostic(range, text, this.getDiagnosticSeverity(diagnostic));
converted.source = diagnostic.source || source;
if (diagnostic.code) {
converted.code = diagnostic.code;
}
const relatedInformation = diagnostic.relatedInformation;
if (relatedInformation) {
converted.relatedInformation = coalesce(relatedInformation.map((info: any) => {
const span = info.span;
if (!span) {
return undefined;
}
return new vscode.DiagnosticRelatedInformation(typeConverters.Location.fromTextSpan(this.client.toResource(span.file), span), info.message);
}));
}
const tags: vscode.DiagnosticTag[] = [];
if (diagnostic.reportsUnnecessary) {
tags.push(vscode.DiagnosticTag.Unnecessary);
}
if (diagnostic.reportsDeprecated) {
tags.push(vscode.DiagnosticTag.Deprecated);
}
converted.tags = tags.length ? tags : undefined;
const resultConverted = converted as vscode.Diagnostic & { reportUnnecessary: any, reportDeprecated: any };
resultConverted.reportUnnecessary = diagnostic.reportsUnnecessary;
resultConverted.reportDeprecated = diagnostic.reportsDeprecated;
return resultConverted;
}
private getDiagnosticSeverity(diagnostic: Proto.Diagnostic): vscode.DiagnosticSeverity {
if (this.reportStyleCheckAsWarnings
&& this.isStyleCheckDiagnostic(diagnostic.code)
&& diagnostic.category === PConst.DiagnosticCategory.error
) {
return vscode.DiagnosticSeverity.Warning;
}
switch (diagnostic.category) {
case PConst.DiagnosticCategory.error:
return vscode.DiagnosticSeverity.Error;
case PConst.DiagnosticCategory.warning:
return vscode.DiagnosticSeverity.Warning;
case PConst.DiagnosticCategory.suggestion:
return vscode.DiagnosticSeverity.Hint;
default:
return vscode.DiagnosticSeverity.Error;
}
}
private isStyleCheckDiagnostic(code: number | undefined): boolean {
return typeof code === 'number' && styleCheckDiagnostics.has(code);
}
}

View File

@@ -0,0 +1,201 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as Proto from './protocol';
import BufferSyncSupport from './tsServer/bufferSyncSupport';
import { ExectuionTarget } from './tsServer/server';
import { TypeScriptVersion } from './tsServer/versionProvider';
import API from './utils/api';
import { TypeScriptServiceConfiguration } from './utils/configuration';
import { PluginManager } from './utils/plugins';
import { TelemetryReporter } from './utils/telemetry';
export enum ServerType {
Syntax = 'syntax',
Semantic = 'semantic',
}
export namespace ServerResponse {
export class Cancelled {
public readonly type = 'cancelled';
constructor(
public readonly reason: string
) { }
}
export const NoContent = { type: 'noContent' } as const;
export type Response<T extends Proto.Response> = T | Cancelled | typeof NoContent;
}
interface StandardTsServerRequests {
'applyCodeActionCommand': [Proto.ApplyCodeActionCommandRequestArgs, Proto.ApplyCodeActionCommandResponse];
'completionEntryDetails': [Proto.CompletionDetailsRequestArgs, Proto.CompletionDetailsResponse];
'completionInfo': [Proto.CompletionsRequestArgs, Proto.CompletionInfoResponse];
'completions': [Proto.CompletionsRequestArgs, Proto.CompletionsResponse];
'configure': [Proto.ConfigureRequestArguments, Proto.ConfigureResponse];
'definition': [Proto.FileLocationRequestArgs, Proto.DefinitionResponse];
'definitionAndBoundSpan': [Proto.FileLocationRequestArgs, Proto.DefinitionInfoAndBoundSpanResponse];
'docCommentTemplate': [Proto.FileLocationRequestArgs, Proto.DocCommandTemplateResponse];
'documentHighlights': [Proto.DocumentHighlightsRequestArgs, Proto.DocumentHighlightsResponse];
'format': [Proto.FormatRequestArgs, Proto.FormatResponse];
'formatonkey': [Proto.FormatOnKeyRequestArgs, Proto.FormatResponse];
'getApplicableRefactors': [Proto.GetApplicableRefactorsRequestArgs, Proto.GetApplicableRefactorsResponse];
'getCodeFixes': [Proto.CodeFixRequestArgs, Proto.CodeFixResponse];
'getCombinedCodeFix': [Proto.GetCombinedCodeFixRequestArgs, Proto.GetCombinedCodeFixResponse];
'getEditsForFileRename': [Proto.GetEditsForFileRenameRequestArgs, Proto.GetEditsForFileRenameResponse];
'getEditsForRefactor': [Proto.GetEditsForRefactorRequestArgs, Proto.GetEditsForRefactorResponse];
'getOutliningSpans': [Proto.FileRequestArgs, Proto.OutliningSpansResponse];
'getSupportedCodeFixes': [null, Proto.GetSupportedCodeFixesResponse];
'implementation': [Proto.FileLocationRequestArgs, Proto.ImplementationResponse];
'jsxClosingTag': [Proto.JsxClosingTagRequestArgs, Proto.JsxClosingTagResponse];
'navto': [Proto.NavtoRequestArgs, Proto.NavtoResponse];
'navtree': [Proto.FileRequestArgs, Proto.NavTreeResponse];
'organizeImports': [Proto.OrganizeImportsRequestArgs, Proto.OrganizeImportsResponse];
'projectInfo': [Proto.ProjectInfoRequestArgs, Proto.ProjectInfoResponse];
'quickinfo': [Proto.FileLocationRequestArgs, Proto.QuickInfoResponse];
'references': [Proto.FileLocationRequestArgs, Proto.ReferencesResponse];
'rename': [Proto.RenameRequestArgs, Proto.RenameResponse];
'selectionRange': [Proto.SelectionRangeRequestArgs, Proto.SelectionRangeResponse];
'signatureHelp': [Proto.SignatureHelpRequestArgs, Proto.SignatureHelpResponse];
'typeDefinition': [Proto.FileLocationRequestArgs, Proto.TypeDefinitionResponse];
'updateOpen': [Proto.UpdateOpenRequestArgs, Proto.Response];
'prepareCallHierarchy': [Proto.FileLocationRequestArgs, Proto.PrepareCallHierarchyResponse];
'provideCallHierarchyIncomingCalls': [Proto.FileLocationRequestArgs, Proto.ProvideCallHierarchyIncomingCallsResponse];
'provideCallHierarchyOutgoingCalls': [Proto.FileLocationRequestArgs, Proto.ProvideCallHierarchyOutgoingCallsResponse];
}
interface NoResponseTsServerRequests {
'open': [Proto.OpenRequestArgs, null];
'close': [Proto.FileRequestArgs, null];
'change': [Proto.ChangeRequestArgs, null];
'compilerOptionsForInferredProjects': [Proto.SetCompilerOptionsForInferredProjectsArgs, null];
'reloadProjects': [null, null];
'configurePlugin': [Proto.ConfigurePluginRequest, Proto.ConfigurePluginResponse];
}
interface AsyncTsServerRequests {
'geterr': [Proto.GeterrRequestArgs, Proto.Response];
'geterrForProject': [Proto.GeterrForProjectRequestArgs, Proto.Response];
}
export type TypeScriptRequests = StandardTsServerRequests & NoResponseTsServerRequests & AsyncTsServerRequests;
export type ExecConfig = {
readonly lowPriority?: boolean;
readonly nonRecoverable?: boolean;
readonly cancelOnResourceChange?: vscode.Uri;
readonly executionTarget?: ExectuionTarget;
};
export enum ClientCapability {
/**
* Basic syntax server. All clients should support this.
*/
Syntax,
/**
* Advanced syntax server that can provide single file IntelliSense.
*/
EnhancedSyntax,
/**
* Complete, multi-file semantic server
*/
Semantic,
}
export class ClientCapabilities {
private readonly capabilities: ReadonlySet<ClientCapability>;
constructor(...capabilities: ClientCapability[]) {
this.capabilities = new Set(capabilities);
}
public has(capability: ClientCapability): boolean {
return this.capabilities.has(capability);
}
}
export interface ITypeScriptServiceClient {
/**
* Convert a resource (VS Code) to a normalized path (TypeScript).
*
* Does not try handling case insensitivity.
*/
normalizedPath(resource: vscode.Uri): string | undefined;
/**
* Map a resource to a normalized path
*
* This will attempt to handle case insensitivity.
*/
toPath(resource: vscode.Uri): string | undefined;
/**
* Convert a path to a resource.
*/
toResource(filepath: string): vscode.Uri;
/**
* Tries to ensure that a vscode document is open on the TS server.
*
* @return The normalized path or `undefined` if the document is not open on the server.
*/
toOpenedFilePath(document: vscode.TextDocument): string | undefined;
/**
* Checks if `resource` has a given capability.
*/
hasCapabilityForResource(resource: vscode.Uri, capability: ClientCapability): boolean;
getWorkspaceRootForResource(resource: vscode.Uri): string | undefined;
readonly onTsServerStarted: vscode.Event<{ version: TypeScriptVersion, usedApiVersion: API }>;
readonly onProjectLanguageServiceStateChanged: vscode.Event<Proto.ProjectLanguageServiceStateEventBody>;
readonly onDidBeginInstallTypings: vscode.Event<Proto.BeginInstallTypesEventBody>;
readonly onDidEndInstallTypings: vscode.Event<Proto.EndInstallTypesEventBody>;
readonly onTypesInstallerInitializationFailed: vscode.Event<Proto.TypesInstallerInitializationFailedEventBody>;
readonly capabilities: ClientCapabilities;
readonly onDidChangeCapabilities: vscode.Event<void>;
onReady(f: () => void): Promise<void>;
showVersionPicker(): void;
readonly apiVersion: API;
readonly pluginManager: PluginManager;
readonly configuration: TypeScriptServiceConfiguration;
readonly bufferSyncSupport: BufferSyncSupport;
readonly telemetryReporter: TelemetryReporter;
execute<K extends keyof StandardTsServerRequests>(
command: K,
args: StandardTsServerRequests[K][0],
token: vscode.CancellationToken,
config?: ExecConfig
): Promise<ServerResponse.Response<StandardTsServerRequests[K][1]>>;
executeWithoutWaitingForResponse<K extends keyof NoResponseTsServerRequests>(
command: K,
args: NoResponseTsServerRequests[K][0]
): void;
executeAsync<K extends keyof AsyncTsServerRequests>(
command: K,
args: AsyncTsServerRequests[K][0],
token: vscode.CancellationToken
): Promise<ServerResponse.Response<Proto.Response>>;
/**
* Cancel on going geterr requests and re-queue them after `f` has been evaluated.
*/
interruptGetErr<R>(f: () => R): R;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
interface ObjectMap<V> {
[key: string]: V;
}

View File

@@ -0,0 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>

View File

@@ -0,0 +1,82 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as semver from 'semver';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export default class API {
public static fromSimpleString(value: string): API {
return new API(value, value, value);
}
public static readonly defaultVersion = API.fromSimpleString('1.0.0');
public static readonly v240 = API.fromSimpleString('2.4.0');
public static readonly v250 = API.fromSimpleString('2.5.0');
public static readonly v260 = API.fromSimpleString('2.6.0');
public static readonly v270 = API.fromSimpleString('2.7.0');
public static readonly v280 = API.fromSimpleString('2.8.0');
public static readonly v290 = API.fromSimpleString('2.9.0');
public static readonly v291 = API.fromSimpleString('2.9.1');
public static readonly v292 = API.fromSimpleString('2.9.2');
public static readonly v300 = API.fromSimpleString('3.0.0');
public static readonly v310 = API.fromSimpleString('3.1.0');
public static readonly v314 = API.fromSimpleString('3.1.4');
public static readonly v320 = API.fromSimpleString('3.2.0');
public static readonly v330 = API.fromSimpleString('3.3.0');
public static readonly v333 = API.fromSimpleString('3.3.3');
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 v380 = API.fromSimpleString('3.8.0');
public static readonly v381 = API.fromSimpleString('3.8.1');
public static readonly v390 = API.fromSimpleString('3.9.0');
public static readonly v400 = API.fromSimpleString('4.0.0');
public static readonly v401 = API.fromSimpleString('4.0.1');
public static fromVersionString(versionString: string): API {
let version = semver.valid(versionString);
if (!version) {
return new API(localize('invalidVersion', 'invalid version'), '1.0.0', '1.0.0');
}
// Cut off any prerelease tag since we sometimes consume those on purpose.
const index = versionString.indexOf('-');
if (index >= 0) {
version = version.substr(0, index);
}
return new API(versionString, version, versionString);
}
private constructor(
/**
* Human readable string for the current version. Displayed in the UI
*/
public readonly displayName: string,
/**
* Semver version, e.g. '3.9.0'
*/
public readonly version: string,
/**
* Full version string including pre-release tags, e.g. '3.9.0-beta'
*/
public readonly fullVersionString: string,
) { }
public eq(other: API): boolean {
return semver.eq(this.version, other.version);
}
public gte(other: API): boolean {
return semver.gte(this.version, other.version);
}
public lt(other: API): boolean {
return !this.gte(other);
}
}

Some files were not shown because too many files have changed in this diff Show More