enable syntactic features if project size exceeded the limit, send events when state of language service changes

This commit is contained in:
Vladimir Matveev 2016-11-11 17:35:11 -08:00
parent 0e879c0bbf
commit 3b09010c7c
9 changed files with 283 additions and 93 deletions

View File

@ -3089,6 +3089,10 @@
"category": "Error",
"code": 9003
},
"Language service is disabled.": {
"category": "Error",
"code": 9004
},
"JSX attributes must only be assigned a non-empty 'expression'.": {
"category": "Error",
"code": 17000

View File

@ -154,7 +154,6 @@ namespace ts.projectSystem {
params.executingFilePath || getExecutingFilePathFromLibFile(),
params.currentDirectory || "/",
fileOrFolderList);
host.createFileOrFolder(safeList, /*createParentDirectory*/ true);
return host;
}
@ -355,7 +354,8 @@ namespace ts.projectSystem {
reloadFS(filesOrFolders: FileOrFolder[]) {
this.filesOrFolders = filesOrFolders;
this.fs = createFileMap<FSEntry>();
for (const fileOrFolder of filesOrFolders) {
// always inject safelist file in the list of files
for (const fileOrFolder of filesOrFolders.concat(safeList)) {
const path = this.toPath(fileOrFolder.path);
const fullPath = getNormalizedAbsolutePath(fileOrFolder.path, this.currentDirectory);
if (typeof fileOrFolder.content === "string") {
@ -1585,6 +1585,104 @@ namespace ts.projectSystem {
projectService.closeClientFile(file1.path);
checkNumberOfProjects(projectService, { configuredProjects: 0 });
});
it("language service disabled events are triggered", () => {
const f1 = {
path: "/a/app.js",
content: "let x = 1;"
};
const f2 = {
path: "/a/largefile.js",
content: ""
};
const config = {
path: "/a/jsconfig.json",
content: "{}"
};
const configWithExclude = {
path: config.path,
content: JSON.stringify({ exclude: ["largefile.js"] })
};
const host = createServerHost([f1, f2, config]);
const originalGetFileSize = host.getFileSize;
host.getFileSize = (filePath: string) =>
filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath);
let lastEvent: server.ProjectLanguageServiceStateEvent;
const session = createSession(host, /*typingsInstaller*/ undefined, e => {
if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ContextEvent) {
return;
}
assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent);
lastEvent = <server.ProjectLanguageServiceStateEvent>e;
});
session.executeCommand(<protocol.OpenRequest>{
seq: 0,
type: "request",
command: "open",
arguments: { file: f1.path }
});
const projectService = session.getProjectService();
checkNumberOfProjects(projectService, { configuredProjects: 1 });
const project = projectService.configuredProjects[0];
assert.isFalse(project.languageServiceEnabled, "Language service enabled");
assert.isTrue(!!lastEvent, "should receive event");
assert.equal(lastEvent.data.project, project, "project name");
assert.isFalse(lastEvent.data.languageServiceEnabled, "Language service state");
host.reloadFS([f1, f2, configWithExclude]);
host.triggerFileWatcherCallback(config.path, /*removed*/ false);
checkNumberOfProjects(projectService, { configuredProjects: 1 });
assert.isTrue(project.languageServiceEnabled, "Language service enabled");
assert.equal(lastEvent.data.project, project, "project");
assert.isTrue(lastEvent.data.languageServiceEnabled, "Language service state");
});
it("syntactic features work even if language service is disabled", () => {
const f1 = {
path: "/a/app.js",
content: "let x = 1;"
};
const f2 = {
path: "/a/largefile.js",
content: ""
};
const config = {
path: "/a/jsconfig.json",
content: "{}"
};
const host = createServerHost([f1, f2, config]);
const originalGetFileSize = host.getFileSize;
host.getFileSize = (filePath: string) =>
filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath);
let lastEvent: server.ProjectLanguageServiceStateEvent;
const session = createSession(host, /*typingsInstaller*/ undefined, e => {
if (e.eventName === server.ConfigFileDiagEvent) {
return;
}
assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent);
lastEvent = <server.ProjectLanguageServiceStateEvent>e;
});
session.executeCommand(<protocol.OpenRequest>{
seq: 0,
type: "request",
command: "open",
arguments: { file: f1.path }
});
const projectService = session.getProjectService();
checkNumberOfProjects(projectService, { configuredProjects: 1 });
const project = projectService.configuredProjects[0];
assert.isFalse(project.languageServiceEnabled, "Language service enabled");
assert.isTrue(!!lastEvent, "should receive event");
assert.equal(lastEvent.data.project, project, "project name");
assert.isFalse(lastEvent.data.languageServiceEnabled, "Language service state");
const options = projectService.getFormatCodeOptions();
const edits = project.getLanguageService().getFormattingEditsForDocument(f1.path, options);
assert.deepEqual(edits, [{ span: createTextSpan(/*start*/ 7, /*length*/ 3), newText: " " }]);
});
});
describe("Proper errors", () => {

View File

@ -10,8 +10,26 @@
namespace ts.server {
export const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024;
export type ProjectServiceEvent =
{ eventName: "context", data: { project: Project, fileName: NormalizedPath } } | { eventName: "configFileDiag", data: { triggerFile: string, configFileName: string, diagnostics: Diagnostic[] } };
export const ContextEvent = "context";
export const ConfigFileDiagEvent = "configFileDiag";
export const ProjectLanguageServiceStateEvent = "projectLanguageServiceState";
export interface ContextEvent {
eventName: typeof ContextEvent;
data: { project: Project; fileName: NormalizedPath };
}
export interface ConfigFileDiagEvent {
eventName: typeof ConfigFileDiagEvent;
data: { triggerFile: string, configFileName: string, diagnostics: Diagnostic[] };
}
export interface ProjectLanguageServiceStateEvent {
eventName: typeof ProjectLanguageServiceStateEvent;
data: { project: Project, languageServiceEnabled: boolean };
}
export type ProjectServiceEvent = ContextEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent;
export interface ProjectServiceEventHandler {
(event: ProjectServiceEvent): void;
@ -282,6 +300,16 @@ namespace ts.server {
return this.compilerOptionsForInferredProjects;
}
onUpdateLanguageServiceStateForProject(project: Project, languageServiceEnabled: boolean) {
if (!this.eventHandler) {
return;
}
this.eventHandler(<ProjectLanguageServiceStateEvent>{
eventName: ProjectLanguageServiceStateEvent,
data: { project, languageServiceEnabled }
});
}
updateTypingsForProject(response: SetTypings | InvalidateCachedTypings): void {
const project = this.findProject(response.projectName);
if (!project) {
@ -430,7 +458,10 @@ namespace ts.server {
}
for (const openFile of this.openFiles) {
this.eventHandler({ eventName: "context", data: { project: openFile.getDefaultProject(), fileName: openFile.fileName } });
this.eventHandler(<ContextEvent>{
eventName: ContextEvent,
data: { project: openFile.getDefaultProject(), fileName: openFile.fileName }
});
}
}
@ -834,8 +865,8 @@ namespace ts.server {
return;
}
this.eventHandler({
eventName: "configFileDiag",
this.eventHandler(<ConfigFileDiagEvent>{
eventName: ConfigFileDiagEvent,
data: { configFileName, diagnostics: diagnostics || [], triggerFile }
});
}
@ -1013,7 +1044,7 @@ namespace ts.server {
const useExistingProject = this.useSingleInferredProject && this.inferredProjects.length;
const project = useExistingProject
? this.inferredProjects[0]
: new InferredProject(this, this.documentRegistry, /*languageServiceEnabled*/ true, this.compilerOptionsForInferredProjects);
: new InferredProject(this, this.documentRegistry, this.compilerOptionsForInferredProjects);
project.addRoot(root);

View File

@ -3,9 +3,9 @@
/// <reference path="scriptInfo.ts" />
namespace ts.server {
export class LSHost implements ts.LanguageServiceHost, ModuleResolutionHost, ServerLanguageServiceHost {
export class LSHost implements ts.LanguageServiceHost, ModuleResolutionHost {
private compilationSettings: ts.CompilerOptions;
private readonly resolvedModuleNames= createFileMap<Map<ResolvedModuleWithFailedLookupLocations>>();
private readonly resolvedModuleNames = createFileMap<Map<ResolvedModuleWithFailedLookupLocations>>();
private readonly resolvedTypeReferenceDirectives = createFileMap<Map<ResolvedTypeReferenceDirectiveWithFailedLookupLocations>>();
private readonly getCanonicalFileName: (fileName: string) => string;

View File

@ -90,16 +90,100 @@ namespace ts.server {
}
}
const emptyResult: any[] = [];
const getEmptyResult = () => emptyResult;
const getUndefined = () => <any>undefined;
export function createNoSemanticFeaturesWrapper(realLanguageService: LanguageService): LanguageService {
return {
cleanupSemanticCache: noop,
getSyntacticDiagnostics: (fileName) =>
fileName ? realLanguageService.getSyntacticDiagnostics(fileName) : emptyResult,
getSemanticDiagnostics: getEmptyResult,
getCompilerOptionsDiagnostics: getEmptyResult,
getSyntacticClassifications: (fileName, span) =>
realLanguageService.getSyntacticClassifications(fileName, span),
getEncodedSyntacticClassifications: (fileName, span) =>
realLanguageService.getEncodedSyntacticClassifications(fileName, span),
getSemanticClassifications: getEmptyResult,
getEncodedSemanticClassifications: () =>
({ spans: [], endOfLineState: EndOfLineState.None }),
getCompletionsAtPosition: getUndefined,
findReferences: getEmptyResult,
getCompletionEntryDetails: getUndefined,
getQuickInfoAtPosition: getUndefined,
findRenameLocations: getEmptyResult,
getNameOrDottedNameSpan: (fileName, startPos, endPos) =>
realLanguageService.getNameOrDottedNameSpan(fileName, startPos, endPos),
getBreakpointStatementAtPosition: (fileName, position) =>
realLanguageService.getBreakpointStatementAtPosition(fileName, position),
getBraceMatchingAtPosition: (fileName, position) =>
realLanguageService.getBraceMatchingAtPosition(fileName, position),
getSignatureHelpItems: getUndefined,
getDefinitionAtPosition: getEmptyResult,
getRenameInfo: () => ({
canRename: false,
localizedErrorMessage: getLocaleSpecificMessage(Diagnostics.Language_service_is_disabled),
displayName: undefined,
fullDisplayName: undefined,
kind: undefined,
kindModifiers: undefined,
triggerSpan: undefined
}),
getTypeDefinitionAtPosition: getUndefined,
getReferencesAtPosition: getEmptyResult,
getDocumentHighlights: getEmptyResult,
getOccurrencesAtPosition: getEmptyResult,
getNavigateToItems: getEmptyResult,
getNavigationBarItems: fileName =>
realLanguageService.getNavigationBarItems(fileName),
getNavigationTree: fileName =>
realLanguageService.getNavigationTree(fileName),
getOutliningSpans: fileName =>
realLanguageService.getOutliningSpans(fileName),
getTodoComments: getEmptyResult,
getIndentationAtPosition: (fileName, position, options) =>
realLanguageService.getIndentationAtPosition(fileName, position, options),
getFormattingEditsForRange: (fileName, start, end, options) =>
realLanguageService.getFormattingEditsForRange(fileName, start, end, options),
getFormattingEditsForDocument: (fileName, options) =>
realLanguageService.getFormattingEditsForDocument(fileName, options),
getFormattingEditsAfterKeystroke: (fileName, position, key, options) =>
realLanguageService.getFormattingEditsAfterKeystroke(fileName, position, key, options),
getDocCommentTemplateAtPosition: (fileName, position) =>
realLanguageService.getDocCommentTemplateAtPosition(fileName, position),
isValidBraceCompletionAtPosition: (fileName, position, openingBrace) =>
realLanguageService.isValidBraceCompletionAtPosition(fileName, position, openingBrace),
getEmitOutput: getUndefined,
getProgram: () =>
realLanguageService.getProgram(),
getNonBoundSourceFile: fileName =>
realLanguageService.getNonBoundSourceFile(fileName),
dispose: () =>
realLanguageService.dispose(),
getCompletionEntrySymbol: getUndefined,
getImplementationAtPosition: getEmptyResult,
getSourceFile: fileName =>
realLanguageService.getSourceFile(fileName),
getCodeFixesAtPosition: getEmptyResult
};
}
export abstract class Project {
private rootFiles: ScriptInfo[] = [];
private rootFilesMap: FileMap<ScriptInfo> = createFileMap<ScriptInfo>();
private lsHost: ServerLanguageServiceHost;
private lsHost: LSHost;
private program: ts.Program;
private cachedUnresolvedImportsPerFile = new UnresolvedImportsMap();
private lastCachedUnresolvedImportsList: SortedReadonlyArray<string>;
private languageService: LanguageService;
private readonly languageService: LanguageService;
// wrapper over the real language service that will suppress all semantic operations
private readonly noSemanticFeaturesLanguageService: LanguageService;
public languageServiceEnabled = true;
builder: Builder;
/**
* Set of files that was returned from the last call to getChangesSinceVersion.
@ -147,7 +231,7 @@ namespace ts.server {
readonly projectService: ProjectService,
private documentRegistry: ts.DocumentRegistry,
hasExplicitListOfFiles: boolean,
public languageServiceEnabled: boolean,
languageServiceEnabled: boolean,
private compilerOptions: CompilerOptions,
public compileOnSaveEnabled: boolean) {
@ -165,10 +249,13 @@ namespace ts.server {
this.compilerOptions.noEmitForJsFiles = true;
}
if (languageServiceEnabled) {
this.enableLanguageService();
}
else {
this.lsHost = new LSHost(this.projectService.host, this, this.projectService.cancellationToken);
this.lsHost.setCompilationSettings(this.compilerOptions);
this.languageService = ts.createLanguageService(this.lsHost, this.documentRegistry);
this.noSemanticFeaturesLanguageService = createNoSemanticFeaturesWrapper(this.languageService);
if (!languageServiceEnabled) {
this.disableLanguageService();
}
@ -184,7 +271,9 @@ namespace ts.server {
if (ensureSynchronized) {
this.updateGraph();
}
return this.languageService;
return this.languageServiceEnabled
? this.languageService
: this.noSemanticFeaturesLanguageService;
}
getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[] {
@ -200,18 +289,20 @@ namespace ts.server {
}
enableLanguageService() {
const lsHost = new LSHost(this.projectService.host, this, this.projectService.cancellationToken);
lsHost.setCompilationSettings(this.compilerOptions);
this.languageService = ts.createLanguageService(lsHost, this.documentRegistry);
this.lsHost = lsHost;
if (this.languageServiceEnabled) {
return;
}
this.languageServiceEnabled = true;
this.projectService.onUpdateLanguageServiceStateForProject(this, /*languageServiceEnabled*/ true);
}
disableLanguageService() {
this.languageService = nullLanguageService;
this.lsHost = nullLanguageServiceHost;
if (!this.languageServiceEnabled) {
return;
}
this.languageService.cleanupSemanticCache();
this.languageServiceEnabled = false;
this.projectService.onUpdateLanguageServiceStateForProject(this, /*languageServiceEnabled*/ false);
}
abstract getProjectName(): string;
@ -676,12 +767,12 @@ namespace ts.server {
// Used to keep track of what directories are watched for this project
directoriesWatchedForTsconfig: string[] = [];
constructor(projectService: ProjectService, documentRegistry: ts.DocumentRegistry, languageServiceEnabled: boolean, compilerOptions: CompilerOptions) {
constructor(projectService: ProjectService, documentRegistry: ts.DocumentRegistry, compilerOptions: CompilerOptions) {
super(ProjectKind.Inferred,
projectService,
documentRegistry,
/*files*/ undefined,
languageServiceEnabled,
/*languageServiceEnabled*/ true,
compilerOptions,
/*compileOnSaveEnabled*/ false);

View File

@ -1814,6 +1814,27 @@ namespace ts.server.protocol {
event: "configFileDiag";
}
export type ProjectLanguageServiceStateEventName = "projectLanguageServiceState";
export interface ProjectLanguageServiceStateEvent extends Event {
event: ProjectLanguageServiceStateEventName;
body?: ProjectLanguageServiceStateEventBody;
}
export interface ProjectLanguageServiceStateEventBody {
/**
* Project name that has changes in the state of language service.
* For configured projects this will be the config file path.
* For external projects this will be the name of the projects specified when project was open.
* For inferred projects this event is not raised.
*/
projectName: string;
/**
* True if language service state switched from disabled to enabled
* and false otherwise.
*/
languageServiceEnabled: boolean;
}
/**
* Arguments for reload request.
*/

View File

@ -199,15 +199,23 @@ namespace ts.server {
private defaultEventHandler(event: ProjectServiceEvent) {
switch (event.eventName) {
case "context":
case ContextEvent:
const { project, fileName } = event.data;
this.projectService.logger.info(`got context event, updating diagnostics for ${fileName}`);
this.updateErrorCheck([{ fileName, project }], this.changeSeq,
(n) => n === this.changeSeq, 100);
break;
case "configFileDiag":
case ConfigFileDiagEvent:
const { triggerFile, configFileName, diagnostics } = event.data;
this.configFileDiagnosticEvent(triggerFile, configFileName, diagnostics);
break;
case ProjectLanguageServiceStateEvent:
const eventName: protocol.ProjectLanguageServiceStateEventName = "projectLanguageServiceState";
this.event(<protocol.ProjectLanguageServiceStateEventBody>{
projectName: event.data.project.getProjectName(),
languageServiceEnabled: event.data.languageServiceEnabled
}, eventName);
break;
}
}

View File

@ -11,7 +11,6 @@ namespace ts.server {
export const emptyArray: ReadonlyArray<any> = [];
export interface Logger {
close(): void;
hasLevel(level: LogLevel): boolean;
@ -160,68 +159,6 @@ namespace ts.server {
}
};
}
function throwLanguageServiceIsDisabledError(): never {
throw new Error("LanguageService is disabled");
}
export const nullLanguageService: LanguageService = {
cleanupSemanticCache: throwLanguageServiceIsDisabledError,
getSyntacticDiagnostics: throwLanguageServiceIsDisabledError,
getSemanticDiagnostics: throwLanguageServiceIsDisabledError,
getCompilerOptionsDiagnostics: throwLanguageServiceIsDisabledError,
getSyntacticClassifications: throwLanguageServiceIsDisabledError,
getEncodedSyntacticClassifications: throwLanguageServiceIsDisabledError,
getSemanticClassifications: throwLanguageServiceIsDisabledError,
getEncodedSemanticClassifications: throwLanguageServiceIsDisabledError,
getCompletionsAtPosition: throwLanguageServiceIsDisabledError,
findReferences: throwLanguageServiceIsDisabledError,
getCompletionEntryDetails: throwLanguageServiceIsDisabledError,
getQuickInfoAtPosition: throwLanguageServiceIsDisabledError,
findRenameLocations: throwLanguageServiceIsDisabledError,
getNameOrDottedNameSpan: throwLanguageServiceIsDisabledError,
getBreakpointStatementAtPosition: throwLanguageServiceIsDisabledError,
getBraceMatchingAtPosition: throwLanguageServiceIsDisabledError,
getSignatureHelpItems: throwLanguageServiceIsDisabledError,
getDefinitionAtPosition: throwLanguageServiceIsDisabledError,
getRenameInfo: throwLanguageServiceIsDisabledError,
getTypeDefinitionAtPosition: throwLanguageServiceIsDisabledError,
getReferencesAtPosition: throwLanguageServiceIsDisabledError,
getDocumentHighlights: throwLanguageServiceIsDisabledError,
getOccurrencesAtPosition: throwLanguageServiceIsDisabledError,
getNavigateToItems: throwLanguageServiceIsDisabledError,
getNavigationBarItems: throwLanguageServiceIsDisabledError,
getNavigationTree: throwLanguageServiceIsDisabledError,
getOutliningSpans: throwLanguageServiceIsDisabledError,
getTodoComments: throwLanguageServiceIsDisabledError,
getIndentationAtPosition: throwLanguageServiceIsDisabledError,
getFormattingEditsForRange: throwLanguageServiceIsDisabledError,
getFormattingEditsForDocument: throwLanguageServiceIsDisabledError,
getFormattingEditsAfterKeystroke: throwLanguageServiceIsDisabledError,
getDocCommentTemplateAtPosition: throwLanguageServiceIsDisabledError,
isValidBraceCompletionAtPosition: throwLanguageServiceIsDisabledError,
getEmitOutput: throwLanguageServiceIsDisabledError,
getProgram: throwLanguageServiceIsDisabledError,
getNonBoundSourceFile: throwLanguageServiceIsDisabledError,
dispose: throwLanguageServiceIsDisabledError,
getCompletionEntrySymbol: throwLanguageServiceIsDisabledError,
getImplementationAtPosition: throwLanguageServiceIsDisabledError,
getSourceFile: throwLanguageServiceIsDisabledError,
getCodeFixesAtPosition: throwLanguageServiceIsDisabledError
};
export interface ServerLanguageServiceHost {
setCompilationSettings(options: CompilerOptions): void;
notifyFileRemoved(info: ScriptInfo): void;
startRecordingFilesWithChangedResolutions(): void;
finishRecordingFilesWithChangedResolutions(): Path[];
}
export const nullLanguageServiceHost: ServerLanguageServiceHost = {
setCompilationSettings: () => undefined,
notifyFileRemoved: () => undefined,
startRecordingFilesWithChangedResolutions: () => undefined,
finishRecordingFilesWithChangedResolutions: () => undefined
};
export interface ProjectOptions {
/**

View File

@ -1205,7 +1205,7 @@ namespace ts {
}
function cleanupSemanticCache(): void {
// TODO: Should we jettison the program (or it's type checker) here?
program = undefined;
}
function dispose(): void {