From 4273fd7d55ecdd9a77c7d3dce526667543b657b7 Mon Sep 17 00:00:00 2001 From: Mine Starks Date: Wed, 24 Oct 2018 10:35:09 -0700 Subject: [PATCH 1/3] configurePlugins command for tsserver Implements #18604 --- src/server/editorServices.ts | 16 ++++- src/server/project.ts | 63 ++++++++++++------- src/server/protocol.ts | 11 ++++ src/server/session.ts | 8 +++ .../reference/api/tsserverlibrary.d.ts | 27 ++++++-- 5 files changed, 97 insertions(+), 28 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 2f5b1d91da537..167e2eff2a9ce 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -471,6 +471,8 @@ namespace ts.server { public readonly globalPlugins: ReadonlyArray; public readonly pluginProbeLocations: ReadonlyArray; public readonly allowLocalPluginLoads: boolean; + private currentPluginConfigOverrides: Map | undefined; + public readonly typesMapLocation: string | undefined; public readonly syntaxOnly?: boolean; @@ -1667,7 +1669,7 @@ namespace ts.server { project.enableLanguageService(); project.watchWildcards(createMapFromTemplate(parsedCommandLine.wildcardDirectories!)); // TODO: GH#18217 } - project.enablePluginsWithOptions(compilerOptions); + project.enablePluginsWithOptions(compilerOptions, this.currentPluginConfigOverrides); const filesToAdd = parsedCommandLine.fileNames.concat(project.getExternalFiles()); this.updateRootAndOptionsOfNonInferredProject(project, filesToAdd, fileNamePropertyReader, compilerOptions, parsedCommandLine.typeAcquisition!, parsedCommandLine.compileOnSave!); // TODO: GH#18217 } @@ -1857,7 +1859,7 @@ namespace ts.server { private createInferredProject(currentDirectory: string | undefined, isSingleInferredProject?: boolean, projectRootPath?: NormalizedPath): InferredProject { const compilerOptions = projectRootPath && this.compilerOptionsForInferredProjectsPerProjectRoot.get(projectRootPath) || this.compilerOptionsForInferredProjects; - const project = new InferredProject(this, this.documentRegistry, compilerOptions, projectRootPath, currentDirectory); + const project = new InferredProject(this, this.documentRegistry, compilerOptions, projectRootPath, currentDirectory, this.currentPluginConfigOverrides); if (isSingleInferredProject) { this.inferredProjects.unshift(project); } @@ -2806,6 +2808,16 @@ namespace ts.server { return false; } + + configurePlugin(args: protocol.ConfigurePluginRequestArguments) { + // For any projects that already have the plugin loaded, configure the plugin + this.forEachEnabledProject(project => project.onPluginConfigurationChanged(args.pluginName, args.configuration)); + + // Also save the current configuration to pass on to any projects that are yet to be loaded. + // If a plugin is configured twice, only the latest configuration will be remembered. + this.currentPluginConfigOverrides = this.currentPluginConfigOverrides || createMap(); + this.currentPluginConfigOverrides.set(args.pluginName, args.configuration); + } } /* @internal */ diff --git a/src/server/project.ts b/src/server/project.ts index e0297c640b82a..1d22a99f8e6a6 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -72,6 +72,12 @@ namespace ts.server { export interface PluginModule { create(createInfo: PluginCreateInfo): LanguageService; getExternalFiles?(proj: Project): string[]; + onConfigurationChanged?(config: any): void; + } + + export interface PluginModuleWithName { + name: string; + module: PluginModule; } export type PluginModuleFactory = (mod: { typescript: typeof ts }) => PluginModule; @@ -92,7 +98,7 @@ namespace ts.server { private program: Program; private externalFiles: SortedReadonlyArray; private missingFilesMap: Map; - private plugins: PluginModule[] = []; + private plugins: PluginModuleWithName[] = []; /*@internal*/ /** @@ -549,9 +555,9 @@ namespace ts.server { getExternalFiles(): SortedReadonlyArray { return toSortedArray(flatMap(this.plugins, plugin => { - if (typeof plugin.getExternalFiles !== "function") return; + if (typeof plugin.module.getExternalFiles !== "function") return; try { - return plugin.getExternalFiles(this); + return plugin.module.getExternalFiles(this); } catch (e) { this.projectService.logger.info(`A plugin threw an exception in getExternalFiles: ${e}`); @@ -1105,7 +1111,7 @@ namespace ts.server { this.rootFilesMap.delete(info.path); } - protected enableGlobalPlugins(options: CompilerOptions) { + protected enableGlobalPlugins(options: CompilerOptions, pluginConfigOverrides: Map | undefined) { const host = this.projectService.host; if (!host.require) { @@ -1128,12 +1134,13 @@ namespace ts.server { // Provide global: true so plugins can detect why they can't find their config this.projectService.logger.info(`Loading global plugin ${globalPluginName}`); - this.enablePlugin({ name: globalPluginName, global: true } as PluginImport, searchPaths); + + this.enablePlugin({ name: globalPluginName, global: true } as PluginImport, searchPaths, pluginConfigOverrides); } } } - protected enablePlugin(pluginConfigEntry: PluginImport, searchPaths: string[]) { + protected enablePlugin(pluginConfigEntry: PluginImport, searchPaths: string[], pluginConfigOverrides: Map | undefined) { this.projectService.logger.info(`Enabling plugin ${pluginConfigEntry.name} from candidate paths: ${searchPaths.join(",")}`); const log = (message: string) => { @@ -1143,6 +1150,14 @@ namespace ts.server { const resolvedModule = firstDefined(searchPaths, searchPath => Project.resolveModule(pluginConfigEntry.name, searchPath, this.projectService.host, log)); if (resolvedModule) { + const configurationOverride = pluginConfigOverrides && pluginConfigOverrides.get(pluginConfigEntry.name); + if (configurationOverride) { + // Preserve the name property since it's immutable + const pluginName = pluginConfigEntry.name; + pluginConfigEntry = configurationOverride; + pluginConfigEntry.name = pluginName; + } + this.enableProxy(resolvedModule, pluginConfigEntry); } else { @@ -1150,11 +1165,6 @@ namespace ts.server { } } - /** Starts a new check for diagnostics. Call this if some file has updated that would cause diagnostics to be changed. */ - refreshDiagnostics() { - this.projectService.sendProjectsUpdatedInBackgroundEvent(); - } - private enableProxy(pluginModuleFactory: PluginModuleFactory, configEntry: PluginImport) { try { if (typeof pluginModuleFactory !== "function") { @@ -1180,12 +1190,26 @@ namespace ts.server { } this.projectService.logger.info(`Plugin validation succeded`); this.languageService = newLS; - this.plugins.push(pluginModule); + this.plugins.push({ name: configEntry.name, module: pluginModule }); } catch (e) { this.projectService.logger.info(`Plugin activation failed: ${e}`); } } + + /*@internal*/ + onPluginConfigurationChanged(pluginName: string, configuration: any) { + this.plugins.filter(plugin => plugin.name === pluginName).forEach(plugin => { + if (plugin.module.onConfigurationChanged) { + plugin.module.onConfigurationChanged(configuration); + } + }); + } + + /** Starts a new check for diagnostics. Call this if some file has updated that would cause diagnostics to be changed. */ + refreshDiagnostics() { + this.projectService.sendProjectsUpdatedInBackgroundEvent(); + } } /** @@ -1241,7 +1265,8 @@ namespace ts.server { documentRegistry: DocumentRegistry, compilerOptions: CompilerOptions, projectRootPath: NormalizedPath | undefined, - currentDirectory: string | undefined) { + currentDirectory: string | undefined, + pluginConfigOverrides: Map | undefined) { super(InferredProject.newName(), ProjectKind.Inferred, projectService, @@ -1257,7 +1282,7 @@ namespace ts.server { if (!projectRootPath && !projectService.useSingleInferredProject) { this.canonicalCurrentDirectory = projectService.toCanonicalFileName(this.currentDirectory); } - this.enableGlobalPlugins(this.getCompilerOptions()); + this.enableGlobalPlugins(this.getCompilerOptions(), pluginConfigOverrides); } addRoot(info: ScriptInfo) { @@ -1402,12 +1427,8 @@ namespace ts.server { return program && program.getResolvedProjectReferences(); } - enablePlugins() { - this.enablePluginsWithOptions(this.getCompilerOptions()); - } - /*@internal*/ - enablePluginsWithOptions(options: CompilerOptions) { + enablePluginsWithOptions(options: CompilerOptions, pluginConfigOverrides: Map | undefined) { const host = this.projectService.host; if (!host.require) { @@ -1428,11 +1449,11 @@ namespace ts.server { // Enable tsconfig-specified plugins if (options.plugins) { for (const pluginConfigEntry of options.plugins) { - this.enablePlugin(pluginConfigEntry, searchPaths); + this.enablePlugin(pluginConfigEntry, searchPaths, pluginConfigOverrides); } } - this.enableGlobalPlugins(options); + this.enableGlobalPlugins(options, pluginConfigOverrides); } /** diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 1a4ee8410144f..499c7cee38420 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -129,6 +129,7 @@ namespace ts.server.protocol { GetEditsForFileRename = "getEditsForFileRename", /* @internal */ GetEditsForFileRenameFull = "getEditsForFileRename-full", + ConfigurePlugin = "configurePlugin" // NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`. } @@ -1368,6 +1369,16 @@ namespace ts.server.protocol { export interface ConfigureResponse extends Response { } + export interface ConfigurePluginRequestArguments { + pluginName: string; + configuration: any; + } + + export interface ConfigurePluginRequest extends Request { + command: CommandTypes.ConfigurePlugin; + arguments: ConfigurePluginRequestArguments; + } + /** * Information found in an "open" request. */ diff --git a/src/server/session.ts b/src/server/session.ts index acc1ebf7b044c..75714e5369ad1 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1946,6 +1946,10 @@ namespace ts.server { this.updateErrorCheck(next, checkList, delay, /*requireOpen*/ false); } + private configurePlugin(args: protocol.ConfigurePluginRequestArguments) { + this.projectService.configurePlugin(args); + } + getCanonicalFileName(fileName: string) { const name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); return normalizePath(name); @@ -2267,6 +2271,10 @@ namespace ts.server { [CommandNames.GetEditsForFileRenameFull]: (request: protocol.GetEditsForFileRenameRequest) => { return this.requiredResponse(this.getEditsForFileRename(request.arguments, /*simplifiedResult*/ false)); }, + [CommandNames.ConfigurePlugin]: (request: protocol.ConfigurePluginRequest) => { + this.configurePlugin(request.arguments); + return this.notRequired(); + } }); public addProtocolHandler(command: string, handler: (request: protocol.Request) => HandlerResponse) { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 21ee605120a2f..b255a258c8e54 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -5668,7 +5668,8 @@ declare namespace ts.server.protocol { GetApplicableRefactors = "getApplicableRefactors", GetEditsForRefactor = "getEditsForRefactor", OrganizeImports = "organizeImports", - GetEditsForFileRename = "getEditsForFileRename" + GetEditsForFileRename = "getEditsForFileRename", + ConfigurePlugin = "configurePlugin" } /** * A TypeScript Server message @@ -6609,6 +6610,14 @@ declare namespace ts.server.protocol { */ interface ConfigureResponse extends Response { } + interface ConfigurePluginRequestArguments { + pluginName: string; + configuration: any; + } + interface ConfigurePluginRequest extends Request { + command: CommandTypes.ConfigurePlugin; + arguments: ConfigurePluginRequestArguments; + } /** * Information found in an "open" request. */ @@ -8035,6 +8044,11 @@ declare namespace ts.server { interface PluginModule { create(createInfo: PluginCreateInfo): LanguageService; getExternalFiles?(proj: Project): string[]; + onConfigurationChanged?(config: any): void; + } + interface PluginModuleWithName { + name: string; + module: PluginModule; } type PluginModuleFactory = (mod: { typescript: typeof ts; @@ -8173,11 +8187,12 @@ declare namespace ts.server { filesToString(writeProjectFileNames: boolean): string; setCompilerOptions(compilerOptions: CompilerOptions): void; protected removeRoot(info: ScriptInfo): void; - protected enableGlobalPlugins(options: CompilerOptions): void; - protected enablePlugin(pluginConfigEntry: PluginImport, searchPaths: string[]): void; + protected enableGlobalPlugins(options: CompilerOptions, pluginConfigOverrides: Map | undefined): void; + protected enablePlugin(pluginConfigEntry: PluginImport, searchPaths: string[], pluginConfigOverrides: Map | undefined): void; + private enableProxy; + onPluginConfigurationChanged(pluginName: string, configuration: any): void; /** Starts a new check for diagnostics. Call this if some file has updated that would cause diagnostics to be changed. */ refreshDiagnostics(): void; - private enableProxy; } /** * If a file is opened and no tsconfig (or jsconfig) is found, @@ -8218,7 +8233,6 @@ declare namespace ts.server { getConfigFilePath(): NormalizedPath; getProjectReferences(): ReadonlyArray; updateReferences(refs: ReadonlyArray | undefined): void; - enablePlugins(): void; /** * Get the errors that dont have any file name associated */ @@ -8463,6 +8477,7 @@ declare namespace ts.server { readonly globalPlugins: ReadonlyArray; readonly pluginProbeLocations: ReadonlyArray; readonly allowLocalPluginLoads: boolean; + private currentPluginConfigOverrides; readonly typesMapLocation: string | undefined; readonly syntaxOnly?: boolean; /** Tracks projects that we have already sent telemetry for. */ @@ -8638,6 +8653,7 @@ declare namespace ts.server { applySafeList(proj: protocol.ExternalProject): NormalizedPath[]; openExternalProject(proj: protocol.ExternalProject): void; hasDeferredExtension(): boolean; + configurePlugin(args: protocol.ConfigurePluginRequestArguments): void; } } declare namespace ts.server { @@ -8808,6 +8824,7 @@ declare namespace ts.server { private convertTextChangeToCodeEdit; private getBraceMatching; private getDiagnosticsForProject; + private configurePlugin; getCanonicalFileName(fileName: string): string; exit(): void; private notRequired; From db914d873917b25c91f29f7faf7db668b97dd44f Mon Sep 17 00:00:00 2001 From: Mine Starks Date: Fri, 26 Oct 2018 17:50:10 -0700 Subject: [PATCH 2/3] Add test for configurePlugin --- src/harness/client.ts | 4 +++ src/harness/fourslash.ts | 18 +++++++++-- src/harness/harnessLanguageService.ts | 30 +++++++++++++++++++ tests/cases/fourslash/fourslash.ts | 4 +++ .../cases/fourslash/server/configurePlugin.ts | 22 ++++++++++++++ 5 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 tests/cases/fourslash/server/configurePlugin.ts diff --git a/src/harness/client.ts b/src/harness/client.ts index d873cd9b4fefc..a5f80eb784df1 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -684,6 +684,10 @@ namespace ts.server { return response.body!.map(entry => this.decodeSpan(entry, fileName)); // TODO: GH#18217 } + configurePlugin(pluginName: string, configuration: any): void { + this.processRequest("configurePlugin", { pluginName, configuration }); + } + getIndentationAtPosition(_fileName: string, _position: number, _options: EditorOptions): number { return notImplemented(); } diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index c06e95339f106..df5ffbefc81a6 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -3400,6 +3400,10 @@ Actual: ${stringify(fullActual)}`); } } } + + public configurePlugin(pluginName: string, configuration: any): void { + (this.languageService).configurePlugin(pluginName, configuration); + } } function updateTextRangeForTextChanges({ pos, end }: ts.TextRange, textChanges: ReadonlyArray): ts.TextRange { @@ -3463,19 +3467,20 @@ Actual: ${stringify(fullActual)}`); function runCode(code: string, state: TestState): void { // Compile and execute the test const wrappedCode = - `(function(test, goTo, verify, edit, debug, format, cancellation, classification, verifyOperationIsCancelled) { + `(function(test, goTo, plugins, verify, edit, debug, format, cancellation, classification, verifyOperationIsCancelled) { ${code} })`; try { const test = new FourSlashInterface.Test(state); const goTo = new FourSlashInterface.GoTo(state); + const plugins = new FourSlashInterface.Plugins(state); const verify = new FourSlashInterface.Verify(state); const edit = new FourSlashInterface.Edit(state); const debug = new FourSlashInterface.Debug(state); const format = new FourSlashInterface.Format(state); const cancellation = new FourSlashInterface.Cancellation(state); const f = eval(wrappedCode); - f(test, goTo, verify, edit, debug, format, cancellation, FourSlashInterface.Classification, verifyOperationIsCancelled); + f(test, goTo, plugins, verify, edit, debug, format, cancellation, FourSlashInterface.Classification, verifyOperationIsCancelled); } catch (err) { throw err; @@ -3975,6 +3980,15 @@ namespace FourSlashInterface { } } + export class Plugins { + constructor (private state: FourSlash.TestState) { + } + + public configurePlugin(pluginName: string, configuration: any): void { + this.state.configurePlugin(pluginName, configuration); + } + } + export class GoTo { constructor(private state: FourSlash.TestState) { } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index e90f29446c387..ee35ab360ca29 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -833,6 +833,36 @@ namespace Harness.LanguageService { error: undefined }; + // Accepts configurations + case "configurable-diagnostic-adder": + let customMessage = "default message"; + return { + module: () => ({ + create(info: ts.server.PluginCreateInfo) { + customMessage = info.config.message; + const proxy = makeDefaultProxy(info); + proxy.getSemanticDiagnostics = filename => { + const prev = info.languageService.getSemanticDiagnostics(filename); + const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; + prev.push({ + category: ts.DiagnosticCategory.Error, + file: sourceFile, + code: 9999, + length: 3, + messageText: customMessage, + start: 0 + }); + return prev; + }; + return proxy; + }, + onConfigurationChanged(config: any) { + customMessage = config.message; + } + }), + error: undefined + }; + default: return { module: undefined, diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index e254b2b51db47..b365ad4e5faf1 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -124,6 +124,9 @@ declare namespace FourSlashInterface { symbolsInScope(range: Range): any[]; setTypesRegistry(map: { [key: string]: void }): void; } + class plugins { + configurePlugin(pluginName: string, configuration: any): void; + } class goTo { marker(name?: string | Marker): void; eachMarker(markers: ReadonlyArray, action: (marker: Marker, index: number) => void): void; @@ -651,6 +654,7 @@ declare namespace FourSlashInterface { } declare function verifyOperationIsCancelled(f: any): void; declare var test: FourSlashInterface.test_; +declare var plugins: FourSlashInterface.plugins; declare var goTo: FourSlashInterface.goTo; declare var verify: FourSlashInterface.verify; declare var edit: FourSlashInterface.edit; diff --git a/tests/cases/fourslash/server/configurePlugin.ts b/tests/cases/fourslash/server/configurePlugin.ts new file mode 100644 index 0000000000000..90745f9314613 --- /dev/null +++ b/tests/cases/fourslash/server/configurePlugin.ts @@ -0,0 +1,22 @@ +/// + +// @Filename: tsconfig.json +//// { +//// "compilerOptions": { +//// "plugins": [ +//// { "name": "configurable-diagnostic-adder" , "message": "configured error" } +//// ] +//// }, +//// "files": ["a.ts"] +//// } + +// @Filename: a.ts +//// let x = [1, 2]; +//// /**/ +//// + +// Test that plugin adds an error message which is able to be configured +goTo.marker(); +verify.getSemanticDiagnostics([{ message: "configured error", code: 9999, range: { pos: 0, end: 3, fileName: "a.ts" } }]); +plugins.configurePlugin("configurable-diagnostic-adder", { message: "new error" }); +verify.getSemanticDiagnostics([{ message: "new error", code: 9999, range: { pos: 0, end: 3, fileName: "a.ts" } }]); From 9bb87ecf2ed6f0101ab0fe9a432c08215e2a5850 Mon Sep 17 00:00:00 2001 From: Mine Starks Date: Mon, 29 Oct 2018 12:44:14 -0700 Subject: [PATCH 3/3] Accept baselines --- tests/baselines/reference/api/tsserverlibrary.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index b255a258c8e54..1197da2e35ba5 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -8190,7 +8190,6 @@ declare namespace ts.server { protected enableGlobalPlugins(options: CompilerOptions, pluginConfigOverrides: Map | undefined): void; protected enablePlugin(pluginConfigEntry: PluginImport, searchPaths: string[], pluginConfigOverrides: Map | undefined): void; private enableProxy; - onPluginConfigurationChanged(pluginName: string, configuration: any): void; /** Starts a new check for diagnostics. Call this if some file has updated that would cause diagnostics to be changed. */ refreshDiagnostics(): void; }