From d3cd393e95e195afd84f8692e158f9904e70e167 Mon Sep 17 00:00:00 2001 From: Ievgen Andrushchak Date: Fri, 23 Mar 2018 21:43:14 +0200 Subject: [PATCH] Enable search of local Typescript Language Service Plugins (fix #45856) (#45858) * Added pluginPaths configuration option to provide additional plugins discovery locations * fixed review comments: added array comparison for tsServerPluginPaths, setting made executable --- extensions/typescript/package.json | 11 +++++ extensions/typescript/package.nls.json | 2 + .../typescript/src/typescriptServiceClient.ts | 16 +++++-- extensions/typescript/src/utils/arrays.ts | 17 ++++++++ .../typescript/src/utils/configuration.ts | 10 ++++- .../src/utils/pluginPathsProvider.ts | 43 +++++++++++++++++++ .../src/utils/relativePathResolver.ts | 21 +++++++++ .../typescript/src/utils/versionProvider.ts | 14 +++--- 8 files changed, 122 insertions(+), 12 deletions(-) create mode 100644 extensions/typescript/src/utils/arrays.ts create mode 100644 extensions/typescript/src/utils/pluginPathsProvider.ts create mode 100644 extensions/typescript/src/utils/relativePathResolver.ts diff --git a/extensions/typescript/package.json b/extensions/typescript/package.json index 2dc0cdb439912..4e48b3ec25a48 100644 --- a/extensions/typescript/package.json +++ b/extensions/typescript/package.json @@ -107,6 +107,17 @@ "description": "%typescript.tsserver.log%", "scope": "window" }, + "typescript.tsserver.pluginPaths": { + "type": "array", + "items": { + "type": "string", + "description": "%typescript.tsserver.pluginPaths.item%" + }, + "default": [], + "description": "%typescript.tsserver.pluginPaths%", + "scope": "window", + "isExecutable": true + }, "typescript.tsserver.trace": { "type": "string", "enum": [ diff --git a/extensions/typescript/package.nls.json b/extensions/typescript/package.nls.json index 9e2c885647d3c..a63addb44831d 100644 --- a/extensions/typescript/package.nls.json +++ b/extensions/typescript/package.nls.json @@ -8,6 +8,8 @@ "typescript.tsdk.desc": "Specifies the folder path containing the tsserver and lib*.d.ts files to use.", "typescript.disableAutomaticTypeAcquisition": "Disables automatic type acquisition. Requires TypeScript >= 2.0.6.", "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. Requires TypeScript >= 2.3.0.", + "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.", diff --git a/extensions/typescript/src/typescriptServiceClient.ts b/extensions/typescript/src/typescriptServiceClient.ts index b1798d93dd422..5c4eb3c4ed1d2 100644 --- a/extensions/typescript/src/typescriptServiceClient.ts +++ b/extensions/typescript/src/typescriptServiceClient.ts @@ -30,6 +30,7 @@ import { inferredProjectConfig } from './utils/tsconfig'; import LogDirectoryProvider from './utils/logDirectoryProvider'; import { disposeAll } from './utils/dipose'; import { DiagnosticKind } from './features/diagnostics'; +import { TypeScriptPluginPathsProvider } from './utils/pluginPathsProvider'; const localize = nls.loadMessageBundle(); @@ -154,6 +155,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient private _onReady?: { promise: Promise; resolve: () => void; reject: () => void; }; private _configuration: TypeScriptServiceConfiguration; private versionProvider: TypeScriptVersionProvider; + private pluginPathsProvider: TypeScriptPluginPathsProvider; private versionPicker: TypeScriptVersionPicker; private tracer: Tracer; @@ -210,6 +212,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient this.callbacks = new CallbackMap(); this._configuration = TypeScriptServiceConfiguration.loadFromWorkspace(); this.versionProvider = new TypeScriptVersionProvider(this._configuration); + this.pluginPathsProvider = new TypeScriptPluginPathsProvider(this._configuration); this.versionPicker = new TypeScriptVersionPicker(this.versionProvider, this.workspaceState); this._apiVersion = API.defaultVersion; @@ -221,6 +224,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient this._configuration = TypeScriptServiceConfiguration.loadFromWorkspace(); this.versionProvider.updateConfiguration(this._configuration); + this.pluginPathsProvider.updateConfiguration(this._configuration); this.tracer.updateConfiguration(); if (this.servicePromise) { @@ -791,7 +795,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient this.tracer.traceEvent(event); this.dispatchEvent(event); } else { - throw new Error('Unknown message type ' + message.type + ' recevied'); + throw new Error('Unknown message type ' + message.type + ' received'); } } finally { this.sendNextRequests(); @@ -938,12 +942,19 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient } if (this.apiVersion.has230Features()) { + const pluginPaths = this.pluginPathsProvider.getPluginPaths(); + if (this.plugins.length) { args.push('--globalPlugins', this.plugins.map(x => x.name).join(',')); + if (currentVersion.path === this.versionProvider.defaultVersion.path) { - args.push('--pluginProbeLocations', this.plugins.map(x => x.path).join(',')); + pluginPaths.push(...this.plugins.map(x => x.path)); } } + + if (pluginPaths.length !== 0) { + args.push('--pluginProbeLocations', pluginPaths.join(',')); + } } if (this.apiVersion.has234Features()) { @@ -961,7 +972,6 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient return args; } - private getDebugPort(): number | undefined { const value = process.env['TSS_DEBUG']; if (value) { diff --git a/extensions/typescript/src/utils/arrays.ts b/extensions/typescript/src/utils/arrays.ts new file mode 100644 index 0000000000000..57dbd54a29c28 --- /dev/null +++ b/extensions/typescript/src/utils/arrays.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +export function equals(one: T[], other: T[], itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean { + if (one.length !== other.length) { + return false; + } + + for (let i = 0, len = one.length; i < len; i++) { + if (!itemEquals(one[i], other[i])) { + return false; + } + } + + return true; +} \ No newline at end of file diff --git a/extensions/typescript/src/utils/configuration.ts b/extensions/typescript/src/utils/configuration.ts index 4976a3ec26a59..9359a2bbed022 100644 --- a/extensions/typescript/src/utils/configuration.ts +++ b/extensions/typescript/src/utils/configuration.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { WorkspaceConfiguration, workspace } from 'vscode'; +import * as arrays from './arrays'; export enum TsServerLogLevel { Off, @@ -47,6 +48,7 @@ export class TypeScriptServiceConfiguration { public readonly localTsdk: string | null; public readonly npmLocation: string | null; public readonly tsServerLogLevel: TsServerLogLevel = TsServerLogLevel.Off; + public readonly tsServerPluginPaths: string[]; public readonly checkJs: boolean; public readonly experimentalDecorators: boolean; public readonly disableAutomaticTypeAcquisition: boolean; @@ -63,6 +65,7 @@ export class TypeScriptServiceConfiguration { this.localTsdk = TypeScriptServiceConfiguration.extractLocalTsdk(configuration); this.npmLocation = TypeScriptServiceConfiguration.readNpmLocation(configuration); this.tsServerLogLevel = TypeScriptServiceConfiguration.readTsServerLogLevel(configuration); + this.tsServerPluginPaths = TypeScriptServiceConfiguration.readTsServerPluginPaths(configuration); this.checkJs = TypeScriptServiceConfiguration.readCheckJs(configuration); this.experimentalDecorators = TypeScriptServiceConfiguration.readExperimentalDecorators(configuration); this.disableAutomaticTypeAcquisition = TypeScriptServiceConfiguration.readDisableAutomaticTypeAcquisition(configuration); @@ -76,7 +79,8 @@ export class TypeScriptServiceConfiguration { && this.tsServerLogLevel === other.tsServerLogLevel && this.checkJs === other.checkJs && this.experimentalDecorators === other.experimentalDecorators - && this.disableAutomaticTypeAcquisition === other.disableAutomaticTypeAcquisition; + && this.disableAutomaticTypeAcquisition === other.disableAutomaticTypeAcquisition + && arrays.equals(this.tsServerPluginPaths, other.tsServerPluginPaths); } private static extractGlobalTsdk(configuration: WorkspaceConfiguration): string | null { @@ -100,6 +104,10 @@ export class TypeScriptServiceConfiguration { return TsServerLogLevel.fromString(setting); } + private static readTsServerPluginPaths(configuration: WorkspaceConfiguration): string[] { + return configuration.get('typescript.tsserver.pluginPaths', []); + } + private static readCheckJs(configuration: WorkspaceConfiguration): boolean { return configuration.get('javascript.implicitProjectConfig.checkJs', false); } diff --git a/extensions/typescript/src/utils/pluginPathsProvider.ts b/extensions/typescript/src/utils/pluginPathsProvider.ts new file mode 100644 index 0000000000000..e2e39d6880117 --- /dev/null +++ b/extensions/typescript/src/utils/pluginPathsProvider.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { workspace } from 'vscode'; + +import { TypeScriptServiceConfiguration } from './configuration'; +import { RelativeWorkspacePathResolver } from './relativePathResolver'; + +export class TypeScriptPluginPathsProvider { + public readonly relativePathResolver: RelativeWorkspacePathResolver = new RelativeWorkspacePathResolver(); + + public constructor( + private configuration: TypeScriptServiceConfiguration + ) { } + + public updateConfiguration(configuration: TypeScriptServiceConfiguration): void { + this.configuration = configuration; + } + + public getPluginPaths(): string[] { + const pluginPaths = []; + for (const pluginPath of this.configuration.tsServerPluginPaths) { + pluginPaths.push(...this.resolvePluginPath(pluginPath)); + } + return pluginPaths; + } + + private resolvePluginPath(pluginPath: string): string[] { + if (path.isAbsolute(pluginPath)) { + return [pluginPath]; + } + + const workspacePath = this.relativePathResolver.asAbsoluteWorkspacePath(pluginPath); + if (workspacePath !== undefined) { + return [workspacePath]; + } + + return (workspace.workspaceFolders || []) + .map(workspaceFolder => path.join(workspaceFolder.uri.fsPath, pluginPath)); + } +} \ No newline at end of file diff --git a/extensions/typescript/src/utils/relativePathResolver.ts b/extensions/typescript/src/utils/relativePathResolver.ts new file mode 100644 index 0000000000000..e424fca126a87 --- /dev/null +++ b/extensions/typescript/src/utils/relativePathResolver.ts @@ -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 path from 'path'; +import { workspace } from 'vscode'; + +export class RelativeWorkspacePathResolver { + public asAbsoluteWorkspacePath(relativePath: string): string | undefined { + for (const root of workspace.workspaceFolders || []) { + const rootPrefixes = [`./${root.name}/`, `${root.name}/`, `.\\${root.name}\\`, `${root.name}\\`]; + for (const rootPrefix of rootPrefixes) { + if (relativePath.startsWith(rootPrefix)) { + return path.join(root.uri.fsPath, relativePath.replace(rootPrefix, '')); + } + } + } + + return undefined; + } +} \ No newline at end of file diff --git a/extensions/typescript/src/utils/versionProvider.ts b/extensions/typescript/src/utils/versionProvider.ts index 4f3d4ed85f414..25ecb3355d1f7 100644 --- a/extensions/typescript/src/utils/versionProvider.ts +++ b/extensions/typescript/src/utils/versionProvider.ts @@ -11,6 +11,7 @@ import * as fs from 'fs'; import { workspace, window } from 'vscode'; import { TypeScriptServiceConfiguration } from './configuration'; +import { RelativeWorkspacePathResolver } from './relativePathResolver'; import API from './api'; @@ -91,6 +92,8 @@ export class TypeScriptVersion { export class TypeScriptVersionProvider { + private readonly relativePathResolver: RelativeWorkspacePathResolver = new RelativeWorkspacePathResolver(); + public constructor( private configuration: TypeScriptServiceConfiguration ) { } @@ -165,14 +168,9 @@ export class TypeScriptVersionProvider { return [new TypeScriptVersion(tsdkPathSetting)]; } - for (const root of workspace.workspaceFolders || []) { - const rootPrefixes = [`./${root.name}/`, `${root.name}/`, `.\\${root.name}\\`, `${root.name}\\`]; - for (const rootPrefix of rootPrefixes) { - if (tsdkPathSetting.startsWith(rootPrefix)) { - const workspacePath = path.join(root.uri.fsPath, tsdkPathSetting.replace(rootPrefix, '')); - return [new TypeScriptVersion(workspacePath, tsdkPathSetting)]; - } - } + const workspacePath = this.relativePathResolver.asAbsoluteWorkspacePath(tsdkPathSetting); + if (workspacePath !== undefined) { + return [new TypeScriptVersion(workspacePath, tsdkPathSetting)]; } return this.loadTypeScriptVersionsFromPath(tsdkPathSetting);