From 62871cc0f9a87344bc6b6e6f90a7b3885209a0d5 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 11 Jul 2017 13:38:12 -0700 Subject: [PATCH] Only update file list when there are changes in the watched directories --- src/compiler/commandLineParser.ts | 90 ++++++++++++------- src/compiler/types.ts | 11 +++ .../unittests/tsserverProjectSystem.ts | 3 +- src/server/editorServices.ts | 71 +++++++-------- src/server/project.ts | 3 + 5 files changed, 107 insertions(+), 71 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 422789f420f..94b6540f16a 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1421,7 +1421,7 @@ namespace ts { const options = extend(existingOptions, parsedConfig.options || {}); options.configFilePath = configFileName; setConfigFileInOptions(options, sourceFile); - const { fileNames, wildcardDirectories } = getFileNames(); + const { fileNames, wildcardDirectories, spec } = getFileNames(); return { options, fileNames, @@ -1429,15 +1429,16 @@ namespace ts { raw, errors, wildcardDirectories, - compileOnSave: !!raw.compileOnSave + compileOnSave: !!raw.compileOnSave, + configFileSpecs: spec }; function getFileNames(): ExpandResult { - let fileNames: string[]; + let filesSpecs: string[]; if (hasProperty(raw, "files")) { if (isArray(raw["files"])) { - fileNames = raw["files"]; - if (fileNames.length === 0) { + filesSpecs = raw["files"]; + if (filesSpecs.length === 0) { createCompilerDiagnosticOnlyIfJson(Diagnostics.The_files_list_in_config_file_0_is_empty, configFileName || "tsconfig.json"); } } @@ -1475,19 +1476,14 @@ namespace ts { } } - if (fileNames === undefined && includeSpecs === undefined) { + if (filesSpecs === undefined && includeSpecs === undefined) { includeSpecs = ["**/*"]; } - const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors, extraFileExtensions, sourceFile); + const result = matchFileNames(filesSpecs, includeSpecs, excludeSpecs, basePath, options, host, errors, extraFileExtensions, sourceFile); if (result.fileNames.length === 0 && !hasProperty(raw, "files") && resolutionStack.length === 0) { - errors.push( - createCompilerDiagnostic( - Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, - configFileName || "tsconfig.json", - JSON.stringify(includeSpecs || []), - JSON.stringify(excludeSpecs || []))); + errors.push(getErrorForNoInputFiles(result.spec, configFileName)); } return result; @@ -1500,6 +1496,14 @@ namespace ts { } } + export function getErrorForNoInputFiles({ includeSpecs, excludeSpecs }: ConfigFileSpecs, configFileName?: string) { + return createCompilerDiagnostic( + Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, + configFileName || "tsconfig.json", + JSON.stringify(includeSpecs || []), + JSON.stringify(excludeSpecs || [])); + } + interface ParsedTsconfig { raw: any; options?: CompilerOptions; @@ -1946,7 +1950,40 @@ namespace ts { * @param host The host used to resolve files and directories. * @param errors An array for diagnostic reporting. */ - function matchFileNames(fileNames: string[], include: string[], exclude: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[], extraFileExtensions: JsFileExtensionInfo[], jsonSourceFile: JsonSourceFile): ExpandResult { + function matchFileNames(filesSpecs: string[], includeSpecs: string[], excludeSpecs: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[], extraFileExtensions: JsFileExtensionInfo[], jsonSourceFile: JsonSourceFile): ExpandResult { + basePath = normalizePath(basePath); + let validatedIncludeSpecs: string[], validatedExcludeSpecs: string[]; + + if (includeSpecs) { + validatedIncludeSpecs = validateSpecs(includeSpecs, errors, /*allowTrailingRecursion*/ false, jsonSourceFile, "include"); + } + + if (excludeSpecs) { + validatedExcludeSpecs = validateSpecs(excludeSpecs, errors, /*allowTrailingRecursion*/ true, jsonSourceFile, "exclude"); + } + + // Wildcard directories (provided as part of a wildcard path) are stored in a + // file map that marks whether it was a regular wildcard match (with a `*` or `?` token), + // or a recursive directory. This information is used by filesystem watchers to monitor for + // new entries in these paths. + const wildcardDirectories = getWildcardDirectories(validatedIncludeSpecs, validatedExcludeSpecs, basePath, host.useCaseSensitiveFileNames); + + const spec: ConfigFileSpecs = { filesSpecs, includeSpecs, excludeSpecs, validatedIncludeSpecs, validatedExcludeSpecs, wildcardDirectories }; + return getFileNamesFromConfigSpecs(spec, basePath, options, host, extraFileExtensions); + } + + /** + * Expands an array of file specifications. + * + * @param fileNames The literal file names to include. + * @param include The wildcard file specifications to include. + * @param exclude The wildcard file specifications to exclude. + * @param basePath The base path for any relative file specifications. + * @param options Compiler options. + * @param host The host used to resolve files and directories. + * @param errors An array for diagnostic reporting. + */ + export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: JsFileExtensionInfo[]): ExpandResult { basePath = normalizePath(basePath); // The exclude spec list is converted into a regular expression, which allows us to quickly @@ -1964,19 +2001,7 @@ namespace ts { // via wildcard, and to handle extension priority. const wildcardFileMap = createMap(); - if (include) { - include = validateSpecs(include, errors, /*allowTrailingRecursion*/ false, jsonSourceFile, "include"); - } - - if (exclude) { - exclude = validateSpecs(exclude, errors, /*allowTrailingRecursion*/ true, jsonSourceFile, "exclude"); - } - - // Wildcard directories (provided as part of a wildcard path) are stored in a - // file map that marks whether it was a regular wildcard match (with a `*` or `?` token), - // or a recursive directory. This information is used by filesystem watchers to monitor for - // new entries in these paths. - const wildcardDirectories = getWildcardDirectories(include, exclude, basePath, host.useCaseSensitiveFileNames); + const { filesSpecs, validatedIncludeSpecs, validatedExcludeSpecs, wildcardDirectories } = spec; // Rather than requery this for each file and filespec, we query the supported extensions // once and store it on the expansion context. @@ -1984,15 +2009,15 @@ namespace ts { // Literal files are always included verbatim. An "include" or "exclude" specification cannot // remove a literal file. - if (fileNames) { - for (const fileName of fileNames) { + if (filesSpecs) { + for (const fileName of filesSpecs) { const file = combinePaths(basePath, fileName); literalFileMap.set(keyMapper(file), file); } } - if (include && include.length > 0) { - for (const file of host.readDirectory(basePath, supportedExtensions, exclude, include, /*depth*/ undefined)) { + if (validatedIncludeSpecs && validatedIncludeSpecs.length > 0) { + for (const file of host.readDirectory(basePath, supportedExtensions, validatedExcludeSpecs, validatedIncludeSpecs, /*depth*/ undefined)) { // If we have already included a literal or wildcard path with a // higher priority extension, we should skip this file. // @@ -2020,7 +2045,8 @@ namespace ts { const wildcardFiles = arrayFrom(wildcardFileMap.values()); return { fileNames: literalFiles.concat(wildcardFiles), - wildcardDirectories + wildcardDirectories, + spec }; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index b1610c76088..88bfff872d5 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3731,6 +3731,7 @@ namespace ts { errors: Diagnostic[]; wildcardDirectories?: MapLike; compileOnSave?: boolean; + configFileSpecs?: ConfigFileSpecs; } export const enum WatchDirectoryFlags { @@ -3738,9 +3739,19 @@ namespace ts { Recursive = 1 << 0, } + export interface ConfigFileSpecs { + filesSpecs: string[]; + includeSpecs: string[]; + excludeSpecs: string[]; + validatedIncludeSpecs: string[]; + validatedExcludeSpecs: string[]; + wildcardDirectories: MapLike; + } + export interface ExpandResult { fileNames: string[]; wildcardDirectories: MapLike; + spec: ConfigFileSpecs; } /* @internal */ diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 51eea84f49b..bd5bfb1841d 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -1774,8 +1774,7 @@ namespace ts.projectSystem { host.reloadFS([file1, file2, configFile]); - host.checkTimeoutQueueLength(1); - host.runQueuedTimeoutCallbacks(); // to execute throttled requests + host.checkTimeoutQueueLength(0); // TODO: update graph scheduling (instead of instant update graph) checkNumberOfProjects(projectService, { configuredProjects: 1 }); checkProjectRootFiles(projectService.configuredProjects[0], [file1.path, file2.path]); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index e5fb2006379..a19ea125ebf 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -617,33 +617,30 @@ namespace ts.server { } this.logger.info(`Detected source file changes: ${fileName}`); - this.throttledOperations.schedule( - project.getConfigFilePath(), - /*delay*/250, - () => this.handleFileAddOrRemoveInWatchedDirectoryOfProject(project, fileName)); - } - private handleFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, triggerFile: string) { - // TODO: (sheetalkamat) this actually doesnt need to re-read the config file from the disk - // it just needs to update the file list of file names - // We might be able to do that by caching the info from first parse and add reusing this with the change in the file path - const { projectOptions, configFileErrors } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath()); - this.reportConfigFileDiagnostics(project.getProjectName(), configFileErrors, triggerFile); - - const newRootFiles = projectOptions.files.map((f => this.getCanonicalFileName(f))); - const currentRootFiles = project.getRootFiles().map((f => this.getCanonicalFileName(f))); - - // We check if the project file list has changed. If so, we update the project. - if (!arrayIsEqualTo(currentRootFiles.sort(), newRootFiles.sort())) { - // For configured projects, the change is made outside the tsconfig file, and - // it is not likely to affect the project for other files opened by the client. We can - // just update the current project. - - this.logger.info("Updating configured project"); - this.updateConfiguredProject(project, projectOptions, configFileErrors); - - // Call refreshInferredProjects to clean up inferred projects we may have - // created for the new files + const configFileSpecs = project.configFileSpecs; + const configFilename = normalizePath(project.getConfigFilePath()); + // TODO: (sheetalkamat) use the host that caches - so we dont do file exists and read directory call + const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilerOptions(), this.host, this.hostConfiguration.extraFileExtensions); + const errors = project.getAllProjectErrors(); + if (result.fileNames.length === 0) { + if (!configFileSpecs.filesSpecs) { + if (!some(errors, error => error.code === Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code)) { + errors.push(getErrorForNoInputFiles(configFileSpecs, configFilename)); + } + if (!some(errors, error => error.code === Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files.code)) { + errors.push(createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename)); + } + } + } + else { + filterMutate(errors, error => + error.code !== Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code && + error.code !== Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files.code); + } + this.updateNonInferredProjectFiles(project, result.fileNames, fileNamePropertyReader); + // TODO: (sheetalkamat) schedule the update graph + if (!project.updateGraph()) { this.refreshInferredProjects(); } } @@ -684,11 +681,6 @@ namespace ts.server { this.reloadProjects(); } - private getCanonicalFileName(fileName: string) { - const name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); - return normalizePath(name); - } - private removeProject(project: Project) { this.logger.info(`remove project: ${project.getRootFiles().toString()}`); @@ -1009,7 +1001,7 @@ namespace ts.server { }; } - return { success, projectOptions, configFileErrors: errors }; + return { success, projectOptions, configFileErrors: errors, configFileSpecs: parsedCommandLine.configFileSpecs }; } private exceededTotalSizeLimitForNonTsFiles(name: string, options: CompilerOptions, fileNames: T[], propertyReader: FilePropertyReader) { @@ -1113,7 +1105,7 @@ namespace ts.server { }); } - private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, configFileErrors: Diagnostic[], clientFileName?: string) { + private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, configFileErrors: Diagnostic[], configFileSpecs: ConfigFileSpecs, clientFileName?: string) { const sizeLimitExceeded = this.exceededTotalSizeLimitForNonTsFiles(configFileName, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader); const project = new ConfiguredProject( configFileName, @@ -1126,6 +1118,7 @@ namespace ts.server { this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition, configFileErrors); + project.configFileSpecs = configFileSpecs; project.watchConfigFile((project, eventKind) => this.onConfigChangedForConfiguredProject(project, eventKind)); project.watchWildcards(projectOptions.wildcardDirectories); project.watchTypeRoots((project, path) => this.onTypeRootFileChanged(project, path)); @@ -1156,15 +1149,15 @@ namespace ts.server { } private openConfigFile(configFileName: NormalizedPath, clientFileName?: string) { - const { success, projectOptions, configFileErrors } = this.convertConfigFileContentToProjectOptions(configFileName); + const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName); if (success) { this.logger.info(`Opened configuration file ${configFileName}`); } - return this.createAndAddConfiguredProject(configFileName, projectOptions, configFileErrors, clientFileName); + return this.createAndAddConfiguredProject(configFileName, projectOptions, configFileErrors, configFileSpecs, clientFileName); } - private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader, newOptions: CompilerOptions, newTypeAcquisition: TypeAcquisition, compileOnSave: boolean, configFileErrors: Diagnostic[]) { + private updateNonInferredProjectFiles(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader) { const projectRootFilesMap = project.getRootFilesMap(); const newRootScriptInfoMap: Map = createMap(); @@ -1219,7 +1212,10 @@ namespace ts.server { } }); } + } + private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader, newOptions: CompilerOptions, newTypeAcquisition: TypeAcquisition, compileOnSave: boolean, configFileErrors: Diagnostic[]) { + this.updateNonInferredProjectFiles(project, newUncheckedFiles, propertyReader); project.setCompilerOptions(newOptions); project.setTypeAcquisition(newTypeAcquisition); @@ -1243,7 +1239,8 @@ namespace ts.server { // note: the returned "success" is true does not mean the "configFileErrors" is empty. // because we might have tolerated the errors and kept going. So always return the configFileErrors // regardless the "success" here is true or not. - const { success, projectOptions, configFileErrors } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath()); + const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath()); + project.configFileSpecs = configFileSpecs; if (!success) { // reset project settings to default this.updateNonInferredProject(project, [], fileNamePropertyReader, {}, {}, /*compileOnSave*/ false, configFileErrors); diff --git a/src/server/project.ts b/src/server/project.ts index 0851b4c64b6..acc826c7730 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -971,6 +971,9 @@ namespace ts.server { private typeRootsWatchers: FileWatcher[]; readonly canonicalConfigFilePath: NormalizedPath; + /*@internal*/ + configFileSpecs: ConfigFileSpecs; + private plugins: PluginModule[] = []; /** Used for configured projects which may have multiple open roots */