Skip to content

Commit

Permalink
Merge pull request #28106 from minestarks/configure-plugins
Browse files Browse the repository at this point in the history
configurePlugins command for tsserver
  • Loading branch information
minestarks committed Oct 29, 2018
2 parents 2cd9eba + 9bb87ec commit 83c38f3
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 30 deletions.
4 changes: 4 additions & 0 deletions src/harness/client.ts
Expand Up @@ -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<protocol.ConfigurePluginRequest>("configurePlugin", { pluginName, configuration });
}

getIndentationAtPosition(_fileName: string, _position: number, _options: EditorOptions): number {
return notImplemented();
}
Expand Down
18 changes: 16 additions & 2 deletions src/harness/fourslash.ts
Expand Up @@ -3399,6 +3399,10 @@ Actual: ${stringify(fullActual)}`);
}
}
}

public configurePlugin(pluginName: string, configuration: any): void {
(<ts.server.SessionClient>this.languageService).configurePlugin(pluginName, configuration);
}
}

function updateTextRangeForTextChanges({ pos, end }: ts.TextRange, textChanges: ReadonlyArray<ts.TextChange>): ts.TextRange {
Expand Down Expand Up @@ -3462,19 +3466,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;
Expand Down Expand Up @@ -3974,6 +3979,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) {
}
Expand Down
30 changes: 30 additions & 0 deletions src/harness/harnessLanguageService.ts
Expand Up @@ -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,
Expand Down
16 changes: 14 additions & 2 deletions src/server/editorServices.ts
Expand Up @@ -471,6 +471,8 @@ namespace ts.server {
public readonly globalPlugins: ReadonlyArray<string>;
public readonly pluginProbeLocations: ReadonlyArray<string>;
public readonly allowLocalPluginLoads: boolean;
private currentPluginConfigOverrides: Map<any> | undefined;

public readonly typesMapLocation: string | undefined;

public readonly syntaxOnly?: boolean;
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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 */
Expand Down
63 changes: 42 additions & 21 deletions src/server/project.ts
Expand Up @@ -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;
Expand All @@ -92,7 +98,7 @@ namespace ts.server {
private program: Program;
private externalFiles: SortedReadonlyArray<string>;
private missingFilesMap: Map<FileWatcher>;
private plugins: PluginModule[] = [];
private plugins: PluginModuleWithName[] = [];

/*@internal*/
/**
Expand Down Expand Up @@ -549,9 +555,9 @@ namespace ts.server {

getExternalFiles(): SortedReadonlyArray<string> {
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}`);
Expand Down Expand Up @@ -1105,7 +1111,7 @@ namespace ts.server {
this.rootFilesMap.delete(info.path);
}

protected enableGlobalPlugins(options: CompilerOptions) {
protected enableGlobalPlugins(options: CompilerOptions, pluginConfigOverrides: Map<any> | undefined) {
const host = this.projectService.host;

if (!host.require) {
Expand All @@ -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<any> | undefined) {
this.projectService.logger.info(`Enabling plugin ${pluginConfigEntry.name} from candidate paths: ${searchPaths.join(",")}`);

const log = (message: string) => {
Expand All @@ -1143,18 +1150,21 @@ namespace ts.server {
const resolvedModule = firstDefined(searchPaths, searchPath =>
<PluginModuleFactory | undefined>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 {
this.projectService.logger.info(`Couldn't find ${pluginConfigEntry.name}`);
}
}

/** 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") {
Expand All @@ -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();
}
}

/**
Expand Down Expand Up @@ -1241,7 +1265,8 @@ namespace ts.server {
documentRegistry: DocumentRegistry,
compilerOptions: CompilerOptions,
projectRootPath: NormalizedPath | undefined,
currentDirectory: string | undefined) {
currentDirectory: string | undefined,
pluginConfigOverrides: Map<any> | undefined) {
super(InferredProject.newName(),
ProjectKind.Inferred,
projectService,
Expand All @@ -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) {
Expand Down Expand Up @@ -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<any> | undefined) {
const host = this.projectService.host;

if (!host.require) {
Expand All @@ -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);
}

/**
Expand Down
11 changes: 11 additions & 0 deletions src/server/protocol.ts
Expand Up @@ -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`.
}
Expand Down Expand Up @@ -1370,6 +1371,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.
*/
Expand Down
8 changes: 8 additions & 0 deletions src/server/session.ts
Expand Up @@ -1953,6 +1953,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);
Expand Down Expand Up @@ -2274,6 +2278,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) {
Expand Down

0 comments on commit 83c38f3

Please sign in to comment.