From 1fbabd5534c707fc74c37583c8a05d507238d87a Mon Sep 17 00:00:00 2001 From: Mine Starks <16928427+minestarks@users.noreply.github.com> Date: Mon, 29 Oct 2018 13:56:51 -0700 Subject: [PATCH] Merge pull request #28106 from minestarks/configure-plugins configurePlugins command for tsserver --- src/harness/client.ts | 4 ++ src/harness/fourslash.ts | 18 +++++- src/harness/harnessLanguageService.ts | 30 +++++++++ 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 | 26 ++++++-- tests/cases/fourslash/fourslash.ts | 4 ++ .../cases/fourslash/server/configurePlugin.ts | 22 +++++++ 10 files changed, 172 insertions(+), 30 deletions(-) create mode 100644 tests/cases/fourslash/server/configurePlugin.ts diff --git a/src/harness/client.ts b/src/harness/client.ts index f59a9ca318884..2035802ca671d 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -694,6 +694,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 a3721dd29f4fb..6a1aa70bf3ea1 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -3076,6 +3076,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 { @@ -3139,19 +3143,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, completion, verifyOperationIsCancelled) { + `(function(test, goTo, plugins, verify, edit, debug, format, cancellation, classification, completion, 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, FourSlashInterface.Completion, verifyOperationIsCancelled); + f(test, goTo, plugins, verify, edit, debug, format, cancellation, FourSlashInterface.Classification, FourSlashInterface.Completion, verifyOperationIsCancelled); } catch (err) { throw err; @@ -3651,6 +3656,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/src/server/editorServices.ts b/src/server/editorServices.ts index 1a7e668b25eb4..4e4f8ca6d78bf 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -509,6 +509,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; @@ -1742,7 +1744,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 } @@ -1932,7 +1934,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); } @@ -2880,6 +2882,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 ecf62b724ec6e..fb6ec4b80fc74 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(flatMapToMutable(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}`); @@ -1111,7 +1117,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) { @@ -1134,12 +1140,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) => { @@ -1149,6 +1156,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 { @@ -1156,11 +1171,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") { @@ -1186,12 +1196,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(); + } } /** @@ -1247,7 +1271,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, @@ -1263,7 +1288,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) { @@ -1419,12 +1444,8 @@ namespace ts.server { return program && program.forEachResolvedProjectReference(cb); } - enablePlugins() { - this.enablePluginsWithOptions(this.getCompilerOptions()); - } - /*@internal*/ - enablePluginsWithOptions(options: CompilerOptions) { + enablePluginsWithOptions(options: CompilerOptions, pluginConfigOverrides: Map | undefined) { const host = this.projectService.host; if (!host.require) { @@ -1445,11 +1466,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 81b7af72e5b67..aad7ba085de4e 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`. } @@ -1375,6 +1376,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 54caf953ee636..49db5562992d9 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1983,6 +1983,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); @@ -2304,6 +2308,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 87bbf1a1cd2f2..ae04e45229a85 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -5685,7 +5685,8 @@ declare namespace ts.server.protocol { GetApplicableRefactors = "getApplicableRefactors", GetEditsForRefactor = "getEditsForRefactor", OrganizeImports = "organizeImports", - GetEditsForFileRename = "getEditsForFileRename" + GetEditsForFileRename = "getEditsForFileRename", + ConfigurePlugin = "configurePlugin" } /** * A TypeScript Server message @@ -6634,6 +6635,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. */ @@ -8080,6 +8089,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; @@ -8218,11 +8232,11 @@ 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; /** 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, @@ -8263,7 +8277,6 @@ declare namespace ts.server { getConfigFilePath(): NormalizedPath; getProjectReferences(): ReadonlyArray | undefined; updateReferences(refs: ReadonlyArray | undefined): void; - enablePlugins(): void; /** * Get the errors that dont have any file name associated */ @@ -8523,6 +8536,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. */ @@ -8698,6 +8712,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 { @@ -8870,6 +8885,7 @@ declare namespace ts.server { private convertTextChangeToCodeEdit; private getBraceMatching; private getDiagnosticsForProject; + private configurePlugin; getCanonicalFileName(fileName: string): string; exit(): void; private notRequired; diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index dd6d5fce27393..d197926554816 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; @@ -636,6 +639,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" } }]);