Skip to content

Commit 83c38f3

Browse files
authored
Merge pull request #28106 from minestarks/configure-plugins
configurePlugins command for tsserver
2 parents 2cd9eba + 9bb87ec commit 83c38f3

File tree

10 files changed

+172
-30
lines changed

10 files changed

+172
-30
lines changed

src/harness/client.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,10 @@ namespace ts.server {
694694
return response.body!.map(entry => this.decodeSpan(entry, fileName)); // TODO: GH#18217
695695
}
696696

697+
configurePlugin(pluginName: string, configuration: any): void {
698+
this.processRequest<protocol.ConfigurePluginRequest>("configurePlugin", { pluginName, configuration });
699+
}
700+
697701
getIndentationAtPosition(_fileName: string, _position: number, _options: EditorOptions): number {
698702
return notImplemented();
699703
}

src/harness/fourslash.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3399,6 +3399,10 @@ Actual: ${stringify(fullActual)}`);
33993399
}
34003400
}
34013401
}
3402+
3403+
public configurePlugin(pluginName: string, configuration: any): void {
3404+
(<ts.server.SessionClient>this.languageService).configurePlugin(pluginName, configuration);
3405+
}
34023406
}
34033407

34043408
function updateTextRangeForTextChanges({ pos, end }: ts.TextRange, textChanges: ReadonlyArray<ts.TextChange>): ts.TextRange {
@@ -3462,19 +3466,20 @@ Actual: ${stringify(fullActual)}`);
34623466
function runCode(code: string, state: TestState): void {
34633467
// Compile and execute the test
34643468
const wrappedCode =
3465-
`(function(test, goTo, verify, edit, debug, format, cancellation, classification, verifyOperationIsCancelled) {
3469+
`(function(test, goTo, plugins, verify, edit, debug, format, cancellation, classification, verifyOperationIsCancelled) {
34663470
${code}
34673471
})`;
34683472
try {
34693473
const test = new FourSlashInterface.Test(state);
34703474
const goTo = new FourSlashInterface.GoTo(state);
3475+
const plugins = new FourSlashInterface.Plugins(state);
34713476
const verify = new FourSlashInterface.Verify(state);
34723477
const edit = new FourSlashInterface.Edit(state);
34733478
const debug = new FourSlashInterface.Debug(state);
34743479
const format = new FourSlashInterface.Format(state);
34753480
const cancellation = new FourSlashInterface.Cancellation(state);
34763481
const f = eval(wrappedCode);
3477-
f(test, goTo, verify, edit, debug, format, cancellation, FourSlashInterface.Classification, verifyOperationIsCancelled);
3482+
f(test, goTo, plugins, verify, edit, debug, format, cancellation, FourSlashInterface.Classification, verifyOperationIsCancelled);
34783483
}
34793484
catch (err) {
34803485
throw err;
@@ -3974,6 +3979,15 @@ namespace FourSlashInterface {
39743979
}
39753980
}
39763981

3982+
export class Plugins {
3983+
constructor (private state: FourSlash.TestState) {
3984+
}
3985+
3986+
public configurePlugin(pluginName: string, configuration: any): void {
3987+
this.state.configurePlugin(pluginName, configuration);
3988+
}
3989+
}
3990+
39773991
export class GoTo {
39783992
constructor(private state: FourSlash.TestState) {
39793993
}

src/harness/harnessLanguageService.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,36 @@ namespace Harness.LanguageService {
833833
error: undefined
834834
};
835835

836+
// Accepts configurations
837+
case "configurable-diagnostic-adder":
838+
let customMessage = "default message";
839+
return {
840+
module: () => ({
841+
create(info: ts.server.PluginCreateInfo) {
842+
customMessage = info.config.message;
843+
const proxy = makeDefaultProxy(info);
844+
proxy.getSemanticDiagnostics = filename => {
845+
const prev = info.languageService.getSemanticDiagnostics(filename);
846+
const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!;
847+
prev.push({
848+
category: ts.DiagnosticCategory.Error,
849+
file: sourceFile,
850+
code: 9999,
851+
length: 3,
852+
messageText: customMessage,
853+
start: 0
854+
});
855+
return prev;
856+
};
857+
return proxy;
858+
},
859+
onConfigurationChanged(config: any) {
860+
customMessage = config.message;
861+
}
862+
}),
863+
error: undefined
864+
};
865+
836866
default:
837867
return {
838868
module: undefined,

src/server/editorServices.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,8 @@ namespace ts.server {
471471
public readonly globalPlugins: ReadonlyArray<string>;
472472
public readonly pluginProbeLocations: ReadonlyArray<string>;
473473
public readonly allowLocalPluginLoads: boolean;
474+
private currentPluginConfigOverrides: Map<any> | undefined;
475+
474476
public readonly typesMapLocation: string | undefined;
475477

476478
public readonly syntaxOnly?: boolean;
@@ -1667,7 +1669,7 @@ namespace ts.server {
16671669
project.enableLanguageService();
16681670
project.watchWildcards(createMapFromTemplate(parsedCommandLine.wildcardDirectories!)); // TODO: GH#18217
16691671
}
1670-
project.enablePluginsWithOptions(compilerOptions);
1672+
project.enablePluginsWithOptions(compilerOptions, this.currentPluginConfigOverrides);
16711673
const filesToAdd = parsedCommandLine.fileNames.concat(project.getExternalFiles());
16721674
this.updateRootAndOptionsOfNonInferredProject(project, filesToAdd, fileNamePropertyReader, compilerOptions, parsedCommandLine.typeAcquisition!, parsedCommandLine.compileOnSave!); // TODO: GH#18217
16731675
}
@@ -1857,7 +1859,7 @@ namespace ts.server {
18571859

18581860
private createInferredProject(currentDirectory: string | undefined, isSingleInferredProject?: boolean, projectRootPath?: NormalizedPath): InferredProject {
18591861
const compilerOptions = projectRootPath && this.compilerOptionsForInferredProjectsPerProjectRoot.get(projectRootPath) || this.compilerOptionsForInferredProjects;
1860-
const project = new InferredProject(this, this.documentRegistry, compilerOptions, projectRootPath, currentDirectory);
1862+
const project = new InferredProject(this, this.documentRegistry, compilerOptions, projectRootPath, currentDirectory, this.currentPluginConfigOverrides);
18611863
if (isSingleInferredProject) {
18621864
this.inferredProjects.unshift(project);
18631865
}
@@ -2806,6 +2808,16 @@ namespace ts.server {
28062808

28072809
return false;
28082810
}
2811+
2812+
configurePlugin(args: protocol.ConfigurePluginRequestArguments) {
2813+
// For any projects that already have the plugin loaded, configure the plugin
2814+
this.forEachEnabledProject(project => project.onPluginConfigurationChanged(args.pluginName, args.configuration));
2815+
2816+
// Also save the current configuration to pass on to any projects that are yet to be loaded.
2817+
// If a plugin is configured twice, only the latest configuration will be remembered.
2818+
this.currentPluginConfigOverrides = this.currentPluginConfigOverrides || createMap();
2819+
this.currentPluginConfigOverrides.set(args.pluginName, args.configuration);
2820+
}
28092821
}
28102822

28112823
/* @internal */

src/server/project.ts

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ namespace ts.server {
7272
export interface PluginModule {
7373
create(createInfo: PluginCreateInfo): LanguageService;
7474
getExternalFiles?(proj: Project): string[];
75+
onConfigurationChanged?(config: any): void;
76+
}
77+
78+
export interface PluginModuleWithName {
79+
name: string;
80+
module: PluginModule;
7581
}
7682

7783
export type PluginModuleFactory = (mod: { typescript: typeof ts }) => PluginModule;
@@ -92,7 +98,7 @@ namespace ts.server {
9298
private program: Program;
9399
private externalFiles: SortedReadonlyArray<string>;
94100
private missingFilesMap: Map<FileWatcher>;
95-
private plugins: PluginModule[] = [];
101+
private plugins: PluginModuleWithName[] = [];
96102

97103
/*@internal*/
98104
/**
@@ -549,9 +555,9 @@ namespace ts.server {
549555

550556
getExternalFiles(): SortedReadonlyArray<string> {
551557
return toSortedArray(flatMap(this.plugins, plugin => {
552-
if (typeof plugin.getExternalFiles !== "function") return;
558+
if (typeof plugin.module.getExternalFiles !== "function") return;
553559
try {
554-
return plugin.getExternalFiles(this);
560+
return plugin.module.getExternalFiles(this);
555561
}
556562
catch (e) {
557563
this.projectService.logger.info(`A plugin threw an exception in getExternalFiles: ${e}`);
@@ -1105,7 +1111,7 @@ namespace ts.server {
11051111
this.rootFilesMap.delete(info.path);
11061112
}
11071113

1108-
protected enableGlobalPlugins(options: CompilerOptions) {
1114+
protected enableGlobalPlugins(options: CompilerOptions, pluginConfigOverrides: Map<any> | undefined) {
11091115
const host = this.projectService.host;
11101116

11111117
if (!host.require) {
@@ -1128,12 +1134,13 @@ namespace ts.server {
11281134

11291135
// Provide global: true so plugins can detect why they can't find their config
11301136
this.projectService.logger.info(`Loading global plugin ${globalPluginName}`);
1131-
this.enablePlugin({ name: globalPluginName, global: true } as PluginImport, searchPaths);
1137+
1138+
this.enablePlugin({ name: globalPluginName, global: true } as PluginImport, searchPaths, pluginConfigOverrides);
11321139
}
11331140
}
11341141
}
11351142

1136-
protected enablePlugin(pluginConfigEntry: PluginImport, searchPaths: string[]) {
1143+
protected enablePlugin(pluginConfigEntry: PluginImport, searchPaths: string[], pluginConfigOverrides: Map<any> | undefined) {
11371144
this.projectService.logger.info(`Enabling plugin ${pluginConfigEntry.name} from candidate paths: ${searchPaths.join(",")}`);
11381145

11391146
const log = (message: string) => {
@@ -1143,18 +1150,21 @@ namespace ts.server {
11431150
const resolvedModule = firstDefined(searchPaths, searchPath =>
11441151
<PluginModuleFactory | undefined>Project.resolveModule(pluginConfigEntry.name, searchPath, this.projectService.host, log));
11451152
if (resolvedModule) {
1153+
const configurationOverride = pluginConfigOverrides && pluginConfigOverrides.get(pluginConfigEntry.name);
1154+
if (configurationOverride) {
1155+
// Preserve the name property since it's immutable
1156+
const pluginName = pluginConfigEntry.name;
1157+
pluginConfigEntry = configurationOverride;
1158+
pluginConfigEntry.name = pluginName;
1159+
}
1160+
11461161
this.enableProxy(resolvedModule, pluginConfigEntry);
11471162
}
11481163
else {
11491164
this.projectService.logger.info(`Couldn't find ${pluginConfigEntry.name}`);
11501165
}
11511166
}
11521167

1153-
/** Starts a new check for diagnostics. Call this if some file has updated that would cause diagnostics to be changed. */
1154-
refreshDiagnostics() {
1155-
this.projectService.sendProjectsUpdatedInBackgroundEvent();
1156-
}
1157-
11581168
private enableProxy(pluginModuleFactory: PluginModuleFactory, configEntry: PluginImport) {
11591169
try {
11601170
if (typeof pluginModuleFactory !== "function") {
@@ -1180,12 +1190,26 @@ namespace ts.server {
11801190
}
11811191
this.projectService.logger.info(`Plugin validation succeded`);
11821192
this.languageService = newLS;
1183-
this.plugins.push(pluginModule);
1193+
this.plugins.push({ name: configEntry.name, module: pluginModule });
11841194
}
11851195
catch (e) {
11861196
this.projectService.logger.info(`Plugin activation failed: ${e}`);
11871197
}
11881198
}
1199+
1200+
/*@internal*/
1201+
onPluginConfigurationChanged(pluginName: string, configuration: any) {
1202+
this.plugins.filter(plugin => plugin.name === pluginName).forEach(plugin => {
1203+
if (plugin.module.onConfigurationChanged) {
1204+
plugin.module.onConfigurationChanged(configuration);
1205+
}
1206+
});
1207+
}
1208+
1209+
/** Starts a new check for diagnostics. Call this if some file has updated that would cause diagnostics to be changed. */
1210+
refreshDiagnostics() {
1211+
this.projectService.sendProjectsUpdatedInBackgroundEvent();
1212+
}
11891213
}
11901214

11911215
/**
@@ -1241,7 +1265,8 @@ namespace ts.server {
12411265
documentRegistry: DocumentRegistry,
12421266
compilerOptions: CompilerOptions,
12431267
projectRootPath: NormalizedPath | undefined,
1244-
currentDirectory: string | undefined) {
1268+
currentDirectory: string | undefined,
1269+
pluginConfigOverrides: Map<any> | undefined) {
12451270
super(InferredProject.newName(),
12461271
ProjectKind.Inferred,
12471272
projectService,
@@ -1257,7 +1282,7 @@ namespace ts.server {
12571282
if (!projectRootPath && !projectService.useSingleInferredProject) {
12581283
this.canonicalCurrentDirectory = projectService.toCanonicalFileName(this.currentDirectory);
12591284
}
1260-
this.enableGlobalPlugins(this.getCompilerOptions());
1285+
this.enableGlobalPlugins(this.getCompilerOptions(), pluginConfigOverrides);
12611286
}
12621287

12631288
addRoot(info: ScriptInfo) {
@@ -1402,12 +1427,8 @@ namespace ts.server {
14021427
return program && program.getResolvedProjectReferences();
14031428
}
14041429

1405-
enablePlugins() {
1406-
this.enablePluginsWithOptions(this.getCompilerOptions());
1407-
}
1408-
14091430
/*@internal*/
1410-
enablePluginsWithOptions(options: CompilerOptions) {
1431+
enablePluginsWithOptions(options: CompilerOptions, pluginConfigOverrides: Map<any> | undefined) {
14111432
const host = this.projectService.host;
14121433

14131434
if (!host.require) {
@@ -1428,11 +1449,11 @@ namespace ts.server {
14281449
// Enable tsconfig-specified plugins
14291450
if (options.plugins) {
14301451
for (const pluginConfigEntry of options.plugins) {
1431-
this.enablePlugin(pluginConfigEntry, searchPaths);
1452+
this.enablePlugin(pluginConfigEntry, searchPaths, pluginConfigOverrides);
14321453
}
14331454
}
14341455

1435-
this.enableGlobalPlugins(options);
1456+
this.enableGlobalPlugins(options, pluginConfigOverrides);
14361457
}
14371458

14381459
/**

src/server/protocol.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ namespace ts.server.protocol {
129129
GetEditsForFileRename = "getEditsForFileRename",
130130
/* @internal */
131131
GetEditsForFileRenameFull = "getEditsForFileRename-full",
132+
ConfigurePlugin = "configurePlugin"
132133

133134
// NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`.
134135
}
@@ -1370,6 +1371,16 @@ namespace ts.server.protocol {
13701371
export interface ConfigureResponse extends Response {
13711372
}
13721373

1374+
export interface ConfigurePluginRequestArguments {
1375+
pluginName: string;
1376+
configuration: any;
1377+
}
1378+
1379+
export interface ConfigurePluginRequest extends Request {
1380+
command: CommandTypes.ConfigurePlugin;
1381+
arguments: ConfigurePluginRequestArguments;
1382+
}
1383+
13731384
/**
13741385
* Information found in an "open" request.
13751386
*/

src/server/session.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1953,6 +1953,10 @@ namespace ts.server {
19531953
this.updateErrorCheck(next, checkList, delay, /*requireOpen*/ false);
19541954
}
19551955

1956+
private configurePlugin(args: protocol.ConfigurePluginRequestArguments) {
1957+
this.projectService.configurePlugin(args);
1958+
}
1959+
19561960
getCanonicalFileName(fileName: string) {
19571961
const name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase();
19581962
return normalizePath(name);
@@ -2274,6 +2278,10 @@ namespace ts.server {
22742278
[CommandNames.GetEditsForFileRenameFull]: (request: protocol.GetEditsForFileRenameRequest) => {
22752279
return this.requiredResponse(this.getEditsForFileRename(request.arguments, /*simplifiedResult*/ false));
22762280
},
2281+
[CommandNames.ConfigurePlugin]: (request: protocol.ConfigurePluginRequest) => {
2282+
this.configurePlugin(request.arguments);
2283+
return this.notRequired();
2284+
}
22772285
});
22782286

22792287
public addProtocolHandler(command: string, handler: (request: protocol.Request) => HandlerResponse) {

0 commit comments

Comments
 (0)