Redesigned directory watchers

This commit is contained in:
zhengbli 2015-10-05 02:58:40 -07:00
parent 98eaeba4f1
commit 7fa26adf28
3 changed files with 182 additions and 59 deletions

View file

@ -700,6 +700,9 @@ namespace ts {
}
export function getBaseFileName(path: string) {
if (!path) {
return undefined;
}
let i = path.lastIndexOf(directorySeparator);
return i < 0 ? path : path.substring(i + 1);
}
@ -723,6 +726,18 @@ namespace ts {
*/
export const supportedExtensions = [".ts", ".tsx", ".d.ts"];
export function isSupportedSourceFileName(fileName: string) {
if (!fileName) { return false; }
let dotIndex = fileName.lastIndexOf(".");
if (dotIndex < 0) {
return false;
}
let extension = fileName.slice(dotIndex, fileName.length);
return supportedExtensions.indexOf(extension) >= 0;
}
const extensionsToRemove = [".d.ts", ".ts", ".js", ".tsx", ".jsx"];
export function removeFileExtension(path: string): string {
for (let ext of extensionsToRemove) {
@ -817,4 +832,25 @@ namespace ts {
Debug.assert(false, message);
}
}
export function doTwoArraysHaveTheSameElements<T>(array1: Array<T>, array2: Array<T>): Boolean {
if (!array1 || !array2) {
return false;
}
if (array1.length != array2.length) {
return false;
}
array1 = array1.sort();
array2 = array2.sort();
for (let i = 0; i < array1.length; i++) {
if (array1[i] != array2[i]) {
return false;
}
}
return true;
}
}

View file

@ -398,7 +398,8 @@ namespace ts {
// and https://github.com/Microsoft/TypeScript/issues/4643), therefore
// if the current node.js version is newer than 4, use `fs.watch` instead.
if (isNode4OrLater()) {
return _fs.watch(fileName, (eventName: string, path: string) => callback(path));
// Note: in node the callback of fs.watch is given only the base file name as a parameter
return _fs.watch(fileName, (eventName: string, baseFileName: string) => callback(fileName));
}
var watchedFile = watchedFileSet.addFile(fileName, callback);
@ -410,8 +411,22 @@ namespace ts {
// Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows
// (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643)
// therefore if the current node.js version is newer than 4, use `fs.watch` instead.
// In watchDirectory we only care about adding and removing files (when event name is
// "rename"); changes made within files are handled by corresponding fileWatchers (when
// event name is "change")
if (isNode4OrLater()) {
return _fs.watch(path, { persisten: true, recursive: !!recursive }, (eventName: string, modifiedPath: string) => callback(modifiedPath));
return _fs.watch(
path,
{ persisten: true, recursive: !!recursive },
(eventName: string, relativeFileName: string) => {
if (eventName == "rename") {
// when deleting a file, the passed baseFileName is null
callback(relativeFileName == null ? null : ts.combinePaths(path, ts.normalizeSlashes(relativeFileName)))
};
}
);
}
// If Node version is older than 4.0, the "recursive" parameter will be ignored

View file

@ -354,11 +354,9 @@ namespace ts.server {
compilerService: CompilerService;
projectFilename: string;
projectFileWatcher: FileWatcher;
// Inferred projects have a collection of non-recursive directory watchers starting
// from the root path (e.g. "C:\" or "/") to the current path;
// while configured projects whose tsconfig files don't have a "files" array have one
// recursive directory watcher starting from the current path
directoryWatchers: FileWatcher[] = [];
directoryWatcher: FileWatcher;
// Used to keep track of what directories are watched for this project
directoriesWatchedForTsconfig: string[] = [];
program: ts.Program;
filenameToSourceFile: ts.Map<ts.SourceFile> = {};
updateGraphSeq = 0;
@ -382,6 +380,10 @@ namespace ts.server {
return this.projectService.openFile(filename, false);
}
getRootFiles() {
return this.compilerService.host.roots.map(info => info.fileName);
}
getFileNames() {
let sourceFiles = this.program.getSourceFiles();
return sourceFiles.map(sourceFile => sourceFile.fileName);
@ -434,13 +436,11 @@ namespace ts.server {
// add a root file to project
addRoot(info: ScriptInfo) {
info.defaultProject = this;
this.compilerService.host.addRoot(info);
}
// remove a root file from project
removeRoot(info: ScriptInfo) {
info.defaultProject = undefined;
this.compilerService.host.removeRoot(info);
}
@ -496,6 +496,11 @@ namespace ts.server {
openFilesReferenced: ScriptInfo[] = [];
// open files that are roots of a configured project
openFileRootsConfigured: ScriptInfo[] = [];
// a path to directory watcher map that detects added tsconfig files
directoryWatchersForTsconfig: ts.Map<FileWatcher> = {};
// count of how many projects are using the directory watcher. If the
// number becomes 0 for a watcher, then we should close it.
directoryWatchersRefCount: ts.Map<number> = {};
hostConfiguration: HostConfiguration;
constructor(public host: ServerHost, public psLogger: Logger, public eventHandler?: ProjectServiceEventHandler) {
@ -538,32 +543,53 @@ namespace ts.server {
}
/**
* This is the callback function when the directory that an inferred project belongs
* to changed. The function looks for newly added tsconfig.json files; if it found one,
* and the tsconfig.json file contains the root file of the current inferred project,
* it will update the project structure.
* This is the callback function when a watched directory has added or removed files.
* @param project the project that associates with this directory watcher
* @param fileName the absolute file name that changed in watched directory
*/
watchedDirectoryChanged(project: Project, path: string) {
if (project.isConfiguredProject()) {
directoryWatchedForSourceFilesChanged(project: Project, fileName: string) {
// If a change was made inside "folder/file", node will trigger the callback twice:
// one with the fileName being "folder/file", and the other one with "folder".
// We don't respond to the second one.
if (fileName && !ts.isSupportedSourceFileName(fileName)) {
return;
}
let configFileName = ts.combinePaths(path, "tsconfig.json");
if (sys.fileExists(configFileName)) {
let {succeeded, projectOptions, error} = this.configFileToProjectOptions(configFileName);
if (!succeeded) {
this.log("Detected source file changes: " + fileName);
let { succeeded, projectOptions, error } = this.configFileToProjectOptions(project.projectFilename);
let newRootFiles = projectOptions.files.map(f => this.getCanonicalFileName(f));
let currentRootFiles = project.getRootFiles().map(f => this.getCanonicalFileName(f));
if (!doTwoArraysHaveTheSameElements(currentRootFiles, newRootFiles)) {
// 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.updateConfiguredProject(project);
// Call updateProjectStructure to clean up inferred projects we may have created for the
// new files
this.updateProjectStructure();
}
}
directoryWatchedForTsconfigChanged(fileName: string) {
if (ts.getBaseFileName(fileName) != "tsconfig.json") {
this.log(fileName + " is not tsconfig.json");
return;
}
this.log("Detected newly added tsconfig file: " + fileName);
let { succeeded, projectOptions, error } = this.configFileToProjectOptions(fileName);
let rootFilesInTsconfig = projectOptions.files.map(f => this.getCanonicalFileName(f));
let openFileRoots = this.openFileRoots.map(s => this.getCanonicalFileName(s.fileName));
for (let openFileRoot of openFileRoots) {
if (rootFilesInTsconfig.indexOf(openFileRoot) >= 0) {
this.reloadProjects();
return;
}
let newProjectFileNames = projectOptions.files.map(f => this.getCanonicalFileName(f));
let rootFiles = project.getRootFiles().map(f => this.getCanonicalFileName(f));
for (let rootFile of rootFiles) {
if (newProjectFileNames.indexOf(rootFile) >= 0) {
this.reloadProjects();
return;
}
}
}
}
@ -573,7 +599,7 @@ namespace ts.server {
}
watchedProjectConfigFileChanged(project: Project) {
this.log("Config File Changed: " + project.projectFilename);
this.log("Config file changed: " + project.projectFilename);
this.updateConfiguredProject(project);
this.updateProjectStructure();
}
@ -613,8 +639,18 @@ namespace ts.server {
let currentPath = ts.getDirectoryPath(root.fileName);
let parentPath = ts.getDirectoryPath(currentPath);
while (currentPath != parentPath) {
// To finish
let directoryWatcher = this.host.watchDirectory(currentPath, p => this.);;
if (!project.projectService.directoryWatchersForTsconfig[currentPath]) {
this.log("Add watcher for: " + currentPath);
project.projectService.directoryWatchersForTsconfig[currentPath] =
this.host.watchDirectory(currentPath, fileName => this.directoryWatchedForTsconfigChanged(fileName));
project.projectService.directoryWatchersRefCount[currentPath] = 1;
}
else {
project.projectService.directoryWatchersRefCount[currentPath] += 1;
}
project.directoriesWatchedForTsconfig.push(currentPath);
currentPath = parentPath;
parentPath = ts.getDirectoryPath(parentPath);
}
project.finishGraph();
@ -663,9 +699,23 @@ namespace ts.server {
this.configuredProjects = configuredProjects;
}
removeConfiguredProject(project: Project) {
project.projectFileWatcher.close();
this.configuredProjects = copyListRemovingItem(project, this.configuredProjects);
removeProject(project: Project) {
this.log("remove project: " + project.getRootFiles().toString());
if (project.isConfiguredProject()) {
project.projectFileWatcher.close();
project.directoryWatcher.close();
this.configuredProjects = copyListRemovingItem(project, this.configuredProjects);
}
else {
for (let directory of project.directoriesWatchedForTsconfig) {
if (!(--project.projectService.directoryWatchersRefCount[directory])) {
this.log("Close directory watcher for: " + directory);
project.projectService.directoryWatchersForTsconfig[directory].close();
project.projectService.directoryWatchersForTsconfig[directory] = undefined;
}
}
this.inferredProjects = copyListRemovingItem(project, this.inferredProjects);
}
let fileNames = project.getFileNames();
for (let fileName of fileNames) {
@ -707,8 +757,7 @@ namespace ts.server {
// if r referenced by the new project
if (info.defaultProject.getSourceFile(r)) {
// remove project rooted at r
this.inferredProjects =
copyListRemovingItem(r.defaultProject, this.inferredProjects);
this.removeProject(r.defaultProject);
// put r in referenced open file list
this.openFilesReferenced.push(r);
// set default project of r to the new project
@ -761,19 +810,14 @@ namespace ts.server {
this.openFileRootsConfigured = openFileRootsConfigured;
}
if (removedProject) {
if (removedProject.isConfiguredProject()) {
this.configuredProjects = copyListRemovingItem(removedProject, this.configuredProjects);
}
else {
this.inferredProjects = copyListRemovingItem(removedProject, this.inferredProjects);
}
this.removeProject(removedProject);
var openFilesReferenced: ScriptInfo[] = [];
var orphanFiles: ScriptInfo[] = [];
// for all open, referenced files f
for (var i = 0, len = this.openFilesReferenced.length; i < len; i++) {
var f = this.openFilesReferenced[i];
// if f was referenced by the removed project, remember it
if (f.defaultProject === removedProject) {
if (f.defaultProject === removedProject || !f.defaultProject) {
f.defaultProject = undefined;
orphanFiles.push(f);
}
@ -817,7 +861,11 @@ namespace ts.server {
return referencingProjects;
}
/**
* This function rebuilds the project for every file opened by the client
*/
reloadProjects() {
this.log("reload projects.");
// First check if there is new tsconfig file added for inferred project roots
for (let info of this.openFileRoots) {
this.openOrUpdateConfiguredProjectForFile(info.fileName);
@ -878,14 +926,25 @@ namespace ts.server {
var rootFile = this.openFileRoots[i];
var rootedProject = rootFile.defaultProject;
var referencingProjects = this.findReferencingProjects(rootFile, rootedProject);
if (referencingProjects.length === 0) {
rootFile.defaultProject = rootedProject;
openFileRoots.push(rootFile);
if (rootFile.defaultProject.isConfiguredProject()) {
// If the root file has already been added into a configured project,
// meaning the original inferred project is gone already.
if (!rootedProject.isConfiguredProject()) {
this.removeProject(rootedProject);
}
this.openFileRootsConfigured.push(rootFile);
}
else {
// remove project from inferred projects list because root captured
this.inferredProjects = copyListRemovingItem(rootedProject, this.inferredProjects);
this.openFilesReferenced.push(rootFile);
if (referencingProjects.length === 0) {
rootFile.defaultProject = rootedProject;
openFileRoots.push(rootFile);
}
else {
// remove project from inferred projects list because root captured
this.removeProject(rootedProject);
this.openFilesReferenced.push(rootFile);
}
}
}
this.openFileRoots = openFileRoots;
@ -897,6 +956,9 @@ namespace ts.server {
this.addOpenFile(unattachedOpenFiles[i]);
}
this.printProjects();
this.log("Current openFileRoots: " + this.openFileRoots.map(s => s.fileName).toString());
this.log("Current openFileRootsConfigured: " + this.openFileRootsConfigured.map(s => s.fileName).toString());
}
getScriptInfo(filename: string) {
@ -970,6 +1032,11 @@ namespace ts.server {
return info;
}
/**
* This function tries to search for a tsconfig.json for the given file. If we found it,
* we first detect if there is already a configured project created for it: if so, we re-read
* the tsconfig file content and update the project; otherwise we create a new one.
*/
openOrUpdateConfiguredProjectForFile(fileName: string) {
let searchPath = ts.normalizePath(getDirectoryPath(fileName));
this.log("Search path: " + searchPath, "Info");
@ -1099,7 +1166,7 @@ namespace ts.server {
return { succeeded: false, error: { errorMsg: "tsconfig option errors" } };
}
else if (parsedCommandLine.fileNames == null) {
return { succeeded: false, error: { errorMsg: "no files found" } }
return { succeeded: false, error: { errorMsg: "no files found" } };
}
else {
var projectOptions: ProjectOptions = {
@ -1118,27 +1185,32 @@ namespace ts.server {
return error;
}
else {
let proj = this.createProject(configFilename, projectOptions);
for (let i = 0, len = projectOptions.files.length; i < len; i++) {
let rootFilename = projectOptions.files[i];
let project = this.createProject(configFilename, projectOptions);
for (let rootFilename of projectOptions.files) {
if (this.host.fileExists(rootFilename)) {
let info = this.openFile(rootFilename, /*openedByClient*/ clientFileName == rootFilename);
proj.addRoot(info);
project.addRoot(info);
}
else {
return { errorMsg: "specified file " + rootFilename + " not found" };
}
}
proj.finishGraph();
proj.projectFileWatcher = this.host.watchFile(configFilename, _ => this.watchedProjectConfigFileChanged(proj));
return { success: true, project: proj };
project.finishGraph();
project.projectFileWatcher = this.host.watchFile(configFilename, _ => this.watchedProjectConfigFileChanged(project));
this.log("Add recursive watcher for: " + ts.getDirectoryPath(configFilename));
project.directoryWatcher = this.host.watchDirectory(
ts.getDirectoryPath(configFilename),
path => this.directoryWatchedForSourceFilesChanged(project, path),
/*recursive*/ true
);
return { success: true, project: project };
}
}
updateConfiguredProject(project: Project) {
if (!this.host.fileExists(project.projectFilename)) {
this.log("Config file deleted");
this.removeConfiguredProject(project);
this.removeProject(project);
}
else {
let { succeeded, projectOptions, error } = this.configFileToProjectOptions(project.projectFilename);