From e68161adfa916ebf2f03d4e2bbdec637001044ef Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Tue, 13 Dec 2016 13:21:32 -0800 Subject: [PATCH] when language service is disabled - build program using only open files (#12809) --- .../unittests/tsserverProjectSystem.ts | 44 +++++++ src/server/builder.ts | 32 ++++- src/server/editorServices.ts | 41 +++++-- src/server/project.ts | 115 ++++-------------- src/server/protocol.ts | 5 + src/server/session.ts | 3 + 6 files changed, 132 insertions(+), 108 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 59652f91d67..783a6016f7a 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -1238,6 +1238,7 @@ namespace ts.projectSystem { projectService.closeExternalProject(externalProjectName); checkNumberOfProjects(projectService, { configuredProjects: 0 }); }); + it("external project with included config file opened after configured project and then closed", () => { const file1 = { path: "/a/b/f1.ts", @@ -1797,6 +1798,49 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { configuredProjects: 0 }); }); + it("language service disabled state is updated in external projects", () => { + debugger + const f1 = { + path: "/a/app.js", + content: "var x = 1" + }; + const f2 = { + path: "/a/largefile.js", + content: "" + }; + const host = createServerHost([f1, f2]); + const originalGetFileSize = host.getFileSize; + host.getFileSize = (filePath: string) => + filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); + + const service = createProjectService(host); + const projectFileName = "/a/proj.csproj"; + + service.openExternalProject({ + projectFileName, + rootFiles: toExternalFiles([f1.path, f2.path]), + options: {} + }); + service.checkNumberOfProjects({ externalProjects: 1 }); + assert.isFalse(service.externalProjects[0].languageServiceEnabled, "language service should be disabled - 1"); + + service.openExternalProject({ + projectFileName, + rootFiles: toExternalFiles([f1.path]), + options: {} + }); + service.checkNumberOfProjects({ externalProjects: 1 }); + assert.isTrue(service.externalProjects[0].languageServiceEnabled, "language service should be enabled"); + + service.openExternalProject({ + projectFileName, + rootFiles: toExternalFiles([f1.path, f2.path]), + options: {} + }); + service.checkNumberOfProjects({ externalProjects: 1 }); + assert.isFalse(service.externalProjects[0].languageServiceEnabled, "language service should be disabled - 2"); + }); + it("language service disabled events are triggered", () => { const f1 = { path: "/a/app.js", diff --git a/src/server/builder.ts b/src/server/builder.ts index 5354e7ed508..60f56dc3241 100644 --- a/src/server/builder.ts +++ b/src/server/builder.ts @@ -75,17 +75,32 @@ namespace ts.server { getFilesAffectedBy(scriptInfo: ScriptInfo): string[]; onProjectUpdateGraph(): void; emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean; + clear(): void; } abstract class AbstractBuilder implements Builder { - private fileInfos = createFileMap(); + /** + * stores set of files from the project. + * NOTE: this field is created on demand and should not be accessed directly. + * Use 'getFileInfos' instead. + */ + private fileInfos_doNotAccessDirectly: FileMap; constructor(public readonly project: Project, private ctor: { new (scriptInfo: ScriptInfo, project: Project): T }) { } + private getFileInfos() { + return this.fileInfos_doNotAccessDirectly || (this.fileInfos_doNotAccessDirectly = createFileMap()); + } + + public clear() { + // drop the existing list - it will be re-created as necessary + this.fileInfos_doNotAccessDirectly = undefined; + } + protected getFileInfo(path: Path): T { - return this.fileInfos.get(path); + return this.getFileInfos().get(path); } protected getOrCreateFileInfo(path: Path): T { @@ -99,19 +114,19 @@ namespace ts.server { } protected getFileInfoPaths(): Path[] { - return this.fileInfos.getKeys(); + return this.getFileInfos().getKeys(); } protected setFileInfo(path: Path, info: T) { - this.fileInfos.set(path, info); + this.getFileInfos().set(path, info); } protected removeFileInfo(path: Path) { - this.fileInfos.remove(path); + this.getFileInfos().remove(path); } protected forEachFileInfo(action: (fileInfo: T) => any) { - this.fileInfos.forEachValue((_path, value) => action(value)); + this.getFileInfos().forEachValue((_path, value) => action(value)); } abstract getFilesAffectedBy(scriptInfo: ScriptInfo): string[]; @@ -231,6 +246,11 @@ namespace ts.server { private projectVersionForDependencyGraph: string; + public clear() { + this.projectVersionForDependencyGraph = undefined; + super.clear(); + } + private getReferencedFileInfos(fileInfo: ModuleBuilderFileInfo): ModuleBuilderFileInfo[] { if (!fileInfo.isExternalModuleOrHasOnlyAmbientExternalModules()) { return []; diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index a70b16a5b1e..00df53e1e35 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -126,7 +126,7 @@ namespace ts.server { } export interface OpenConfiguredProjectResult { - configFileName?: string; + configFileName?: NormalizedPath; configFileErrors?: Diagnostic[]; } @@ -659,6 +659,13 @@ namespace ts.server { // open file in inferred project (projectsToRemove || (projectsToRemove = [])).push(p); } + + if (!p.languageServiceEnabled) { + // if project language service is disabled then we create a program only for open files. + // this means that project should be marked as dirty to force rebuilding of the program + // on the next request + p.markAsDirty(); + } } if (projectsToRemove) { for (const project of projectsToRemove) { @@ -1052,9 +1059,7 @@ namespace ts.server { project.stopWatchingDirectory(); } else { - if (!project.languageServiceEnabled) { - project.enableLanguageService(); - } + project.enableLanguageService(); this.watchConfigDirectoryForProject(project, projectOptions); this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typeAcquisition, projectOptions.compileOnSave, configFileErrors); } @@ -1226,9 +1231,22 @@ namespace ts.server { } openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean): OpenConfiguredProjectResult { - const { configFileName = undefined, configFileErrors = undefined }: OpenConfiguredProjectResult = this.findContainingExternalProject(fileName) - ? {} - : this.openOrUpdateConfiguredProjectForFile(fileName); + let configFileName: NormalizedPath; + let configFileErrors: Diagnostic[]; + + let project: ConfiguredProject | ExternalProject = this.findContainingExternalProject(fileName); + if (!project) { + ({ configFileName, configFileErrors } = this.openOrUpdateConfiguredProjectForFile(fileName)); + if (configFileName) { + project = this.findConfiguredProjectByProjectName(configFileName); + } + } + if (project && !project.languageServiceEnabled) { + // if project language service is disabled then we create a program only for open files. + // this means that project should be marked as dirty to force rebuilding of the program + // on the next request + project.markAsDirty(); + } // at this point if file is the part of some configured/external project then this project should be created const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent); @@ -1392,8 +1410,15 @@ namespace ts.server { let exisingConfigFiles: string[]; if (externalProject) { if (!tsConfigFiles) { + const compilerOptions = convertCompilerOptions(proj.options); + if (this.exceededTotalSizeLimitForNonTsFiles(compilerOptions, proj.rootFiles, externalFilePropertyReader)) { + externalProject.disableLanguageService(); + } + else { + externalProject.enableLanguageService(); + } // external project already exists and not config files were added - update the project and return; - this.updateNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, convertCompilerOptions(proj.options), proj.typeAcquisition, proj.options.compileOnSave, /*configFileErrors*/ undefined); + this.updateNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, compilerOptions, proj.typeAcquisition, proj.options.compileOnSave, /*configFileErrors*/ undefined); return; } // some config files were added to external project (that previously were not there) diff --git a/src/server/project.ts b/src/server/project.ts index 392008a9fd6..0037a470a49 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -90,87 +90,6 @@ namespace ts.server { } } - const emptyResult: any[] = []; - const getEmptyResult = () => emptyResult; - const getUndefined = () => undefined; - const emptyEncodedSemanticClassifications = { spans: emptyResult, endOfLineState: EndOfLineState.None }; - - export function createNoSemanticFeaturesWrapper(realLanguageService: LanguageService): LanguageService { - return { - cleanupSemanticCache: noop, - getSyntacticDiagnostics: (fileName) => - fileName ? realLanguageService.getSyntacticDiagnostics(fileName) : emptyResult, - getSemanticDiagnostics: getEmptyResult, - getCompilerOptionsDiagnostics: () => - realLanguageService.getCompilerOptionsDiagnostics(), - getSyntacticClassifications: (fileName, span) => - realLanguageService.getSyntacticClassifications(fileName, span), - getEncodedSyntacticClassifications: (fileName, span) => - realLanguageService.getEncodedSyntacticClassifications(fileName, span), - getSemanticClassifications: getEmptyResult, - getEncodedSemanticClassifications: () => - emptyEncodedSemanticClassifications, - 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 = createFileMap(); @@ -181,8 +100,6 @@ namespace ts.server { private lastCachedUnresolvedImportsList: SortedReadonlyArray; private readonly languageService: LanguageService; - // wrapper over the real language service that will suppress all semantic operations - private readonly noSemanticFeaturesLanguageService: LanguageService; public languageServiceEnabled = true; @@ -258,7 +175,6 @@ namespace ts.server { this.lsHost.setCompilationSettings(this.compilerOptions); this.languageService = ts.createLanguageService(this.lsHost, this.documentRegistry); - this.noSemanticFeaturesLanguageService = createNoSemanticFeaturesWrapper(this.languageService); if (!languageServiceEnabled) { this.disableLanguageService(); @@ -282,9 +198,7 @@ namespace ts.server { if (ensureSynchronized) { this.updateGraph(); } - return this.languageServiceEnabled - ? this.languageService - : this.noSemanticFeaturesLanguageService; + return this.languageService; } getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[] { @@ -373,7 +287,10 @@ namespace ts.server { const result: string[] = []; if (this.rootFiles) { for (const f of this.rootFiles) { - result.push(f.fileName); + if (this.languageServiceEnabled || f.isScriptOpen()) { + // if language service is disabled - process only files that are open + result.push(f.fileName); + } } if (this.typingFiles) { for (const f of this.typingFiles) { @@ -389,6 +306,10 @@ namespace ts.server { } getScriptInfos() { + if (!this.languageServiceEnabled) { + // if language service is not enabled - return just root files + return this.rootFiles; + } return map(this.program.getSourceFiles(), sourceFile => { const scriptInfo = this.projectService.getScriptInfoForPath(sourceFile.path); if (!scriptInfo) { @@ -529,10 +450,6 @@ namespace ts.server { * @returns: true if set of files in the project stays the same and false - otherwise. */ updateGraph(): boolean { - if (!this.languageServiceEnabled) { - return true; - } - this.lsHost.startRecordingFilesWithChangedResolutions(); let hasChanges = this.updateGraphWorker(); @@ -564,6 +481,16 @@ namespace ts.server { if (this.setTypings(cachedTypings)) { hasChanges = this.updateGraphWorker() || hasChanges; } + + // update builder only if language service is enabled + // otherwise tell it to drop its internal state + if (this.languageServiceEnabled) { + this.builder.onProjectUpdateGraph(); + } + else { + this.builder.clear(); + } + if (hasChanges) { this.projectStructureVersion++; } @@ -602,7 +529,6 @@ namespace ts.server { } } } - this.builder.onProjectUpdateGraph(); return hasChanges; } @@ -673,7 +599,8 @@ namespace ts.server { projectName: this.getProjectName(), version: this.projectStructureVersion, isInferred: this.projectKind === ProjectKind.Inferred, - options: this.getCompilerOptions() + options: this.getCompilerOptions(), + languageServiceDisabled: !this.languageServiceEnabled }; const updatedFileNames = this.updatedFileNames; this.updatedFileNames = undefined; diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 27faf417282..39012e49fcd 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -904,6 +904,11 @@ namespace ts.server.protocol { * Current set of compiler options for project */ options: ts.CompilerOptions; + + /** + * true if project language service is disabled + */ + languageServiceDisabled: boolean; } /** diff --git a/src/server/session.ts b/src/server/session.ts index a1944e5e782..5c382aae7d3 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1026,6 +1026,9 @@ namespace ts.server { if (!project) { Errors.ThrowNoProject(); } + if (!project.languageServiceEnabled) { + return false; + } const scriptInfo = project.getScriptInfo(file); return project.builder.emitFile(scriptInfo, (path, data, writeByteOrderMark) => this.host.writeFile(path, data, writeByteOrderMark)); }