Merge pull request #28106 from minestarks/configure-plugins

configurePlugins command for tsserver
This commit is contained in:
Mine Starks
2018-10-29 13:56:51 -07:00
committed by GitHub
10 changed files with 172 additions and 30 deletions

View File

@@ -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();
}

View File

@@ -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 {
@@ -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;
@@ -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) {
}

View File

@@ -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,

View File

@@ -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;
@@ -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 */

View File

@@ -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<string>;
private missingFilesMap: Map<FileWatcher>;
private plugins: PluginModule[] = [];
private plugins: PluginModuleWithName[] = [];
/*@internal*/
/**
@@ -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}`);
@@ -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) {
@@ -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) => {
@@ -1143,6 +1150,14 @@ 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 {
@@ -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<any> | 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<any> | 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);
}
/**

View File

@@ -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`.
}
@@ -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.
*/

View File

@@ -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);
@@ -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) {

View File

@@ -5673,7 +5673,8 @@ declare namespace ts.server.protocol {
GetApplicableRefactors = "getApplicableRefactors",
GetEditsForRefactor = "getEditsForRefactor",
OrganizeImports = "organizeImports",
GetEditsForFileRename = "getEditsForFileRename"
GetEditsForFileRename = "getEditsForFileRename",
ConfigurePlugin = "configurePlugin"
}
/**
* A TypeScript Server message
@@ -6618,6 +6619,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.
*/
@@ -8044,6 +8053,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;
@@ -8182,11 +8196,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<any> | undefined): void;
protected enablePlugin(pluginConfigEntry: PluginImport, searchPaths: string[], pluginConfigOverrides: Map<any> | 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,
@@ -8227,7 +8241,6 @@ declare namespace ts.server {
getConfigFilePath(): NormalizedPath;
getProjectReferences(): ReadonlyArray<ProjectReference>;
updateReferences(refs: ReadonlyArray<ProjectReference> | undefined): void;
enablePlugins(): void;
/**
* Get the errors that dont have any file name associated
*/
@@ -8472,6 +8485,7 @@ declare namespace ts.server {
readonly globalPlugins: ReadonlyArray<string>;
readonly pluginProbeLocations: ReadonlyArray<string>;
readonly allowLocalPluginLoads: boolean;
private currentPluginConfigOverrides;
readonly typesMapLocation: string | undefined;
readonly syntaxOnly?: boolean;
/** Tracks projects that we have already sent telemetry for. */
@@ -8647,6 +8661,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 {
@@ -8817,6 +8832,7 @@ declare namespace ts.server {
private convertTextChangeToCodeEdit;
private getBraceMatching;
private getDiagnosticsForProject;
private configurePlugin;
getCanonicalFileName(fileName: string): string;
exit(): void;
private notRequired;

View File

@@ -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<string>, 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;

View File

@@ -0,0 +1,22 @@
/// <reference path="../fourslash.ts"/>
// @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" } }]);