Redesigned directory watchers
This commit is contained in:
parent
98eaeba4f1
commit
7fa26adf28
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue