mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-16 07:13:45 -05:00
Merge branch 'master' into tscJsFiles
This commit is contained in:
@@ -78,19 +78,19 @@ namespace ts.server {
|
||||
return this.snap().getChangeRange(oldSnapshot);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface TimestampedResolvedModule extends ResolvedModuleWithFailedLookupLocations {
|
||||
lastCheckTime: number;
|
||||
lastCheckTime: number;
|
||||
}
|
||||
|
||||
|
||||
export class LSHost implements ts.LanguageServiceHost {
|
||||
ls: ts.LanguageService = null;
|
||||
compilationSettings: ts.CompilerOptions;
|
||||
filenameToScript: ts.Map<ScriptInfo> = {};
|
||||
roots: ScriptInfo[] = [];
|
||||
private resolvedModuleNames: ts.FileMap<Map<TimestampedResolvedModule>>;
|
||||
private resolvedModuleNames: ts.FileMap<Map<TimestampedResolvedModule>>;
|
||||
private moduleResolutionHost: ts.ModuleResolutionHost;
|
||||
|
||||
|
||||
constructor(public host: ServerHost, public project: Project) {
|
||||
this.resolvedModuleNames = ts.createFileMap<Map<TimestampedResolvedModule>>(ts.createGetCanonicalFileName(host.useCaseSensitiveFileNames))
|
||||
this.moduleResolutionHost = {
|
||||
@@ -98,15 +98,15 @@ namespace ts.server {
|
||||
readFile: fileName => this.host.readFile(fileName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModule[] {
|
||||
let currentResolutionsInFile = this.resolvedModuleNames.get(containingFile);
|
||||
|
||||
|
||||
let newResolutions: Map<TimestampedResolvedModule> = {};
|
||||
let resolvedModules: ResolvedModule[] = [];
|
||||
|
||||
|
||||
let compilerOptions = this.getCompilationSettings();
|
||||
|
||||
|
||||
for (let moduleName of moduleNames) {
|
||||
// check if this is a duplicate entry in the list
|
||||
let resolution = lookUp(newResolutions, moduleName);
|
||||
@@ -122,21 +122,21 @@ namespace ts.server {
|
||||
newResolutions[moduleName] = resolution;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ts.Debug.assert(resolution !== undefined);
|
||||
|
||||
|
||||
resolvedModules.push(resolution.resolvedModule);
|
||||
}
|
||||
|
||||
// replace old results with a new one
|
||||
this.resolvedModuleNames.set(containingFile, newResolutions);
|
||||
return resolvedModules;
|
||||
|
||||
|
||||
function moduleResolutionIsValid(resolution: TimestampedResolvedModule): boolean {
|
||||
if (!resolution) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (resolution.resolvedModule) {
|
||||
// TODO: consider checking failedLookupLocations
|
||||
// TODO: use lastCheckTime to track expiration for module name resolution
|
||||
@@ -147,7 +147,7 @@ namespace ts.server {
|
||||
// after all there is no point to invalidate it if we have no idea where to look for the module.
|
||||
return resolution.failedLookupLocations.length === 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getDefaultLibFileName() {
|
||||
var nodeModuleBinDir = ts.getDirectoryPath(ts.normalizePath(this.host.getExecutingFilePath()));
|
||||
@@ -224,12 +224,13 @@ namespace ts.server {
|
||||
this.roots.push(info);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
removeRoot(info: ScriptInfo) {
|
||||
var scriptInfo = ts.lookUp(this.filenameToScript, info.fileName);
|
||||
if (scriptInfo) {
|
||||
this.filenameToScript[info.fileName] = undefined;
|
||||
this.roots = copyListRemovingItem(info, this.roots);
|
||||
this.resolvedModuleNames.remove(info.fileName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,6 +355,9 @@ namespace ts.server {
|
||||
compilerService: CompilerService;
|
||||
projectFilename: string;
|
||||
projectFileWatcher: 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;
|
||||
@@ -377,6 +381,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);
|
||||
@@ -429,13 +437,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);
|
||||
}
|
||||
|
||||
@@ -491,7 +497,13 @@ 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;
|
||||
timerForDetectingProjectFilelistChanges: Map<NodeJS.Timer> = {};
|
||||
|
||||
constructor(public host: ServerHost, public psLogger: Logger, public eventHandler?: ProjectServiceEventHandler) {
|
||||
// ts.disableIncrementalParsing = true;
|
||||
@@ -532,8 +544,82 @@ namespace ts.server {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the callback function when a watched directory has added or removed source code files.
|
||||
* @param project the project that associates with this directory watcher
|
||||
* @param fileName the absolute file name that changed in watched directory
|
||||
*/
|
||||
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, project.projectOptions ? project.projectOptions.compilerOptions : undefined)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log("Detected source file changes: " + fileName);
|
||||
this.startTimerForDetectingProjectFilelistChanges(project);
|
||||
}
|
||||
|
||||
startTimerForDetectingProjectFilelistChanges(project: Project) {
|
||||
if (this.timerForDetectingProjectFilelistChanges[project.projectFilename]) {
|
||||
clearTimeout(this.timerForDetectingProjectFilelistChanges[project.projectFilename]);
|
||||
}
|
||||
this.timerForDetectingProjectFilelistChanges[project.projectFilename] = setTimeout(
|
||||
() => this.handleProjectFilelistChanges(project),
|
||||
250
|
||||
);
|
||||
}
|
||||
|
||||
handleProjectFilelistChanges(project: Project) {
|
||||
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 (!arrayStructurallyIsEqualTo(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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the callback function when a watched directory has an added tsconfig file.
|
||||
*/
|
||||
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));
|
||||
|
||||
// We should only care about the new tsconfig file if it contains any
|
||||
// opened root files of existing inferred projects
|
||||
for (let openFileRoot of openFileRoots) {
|
||||
if (rootFilesInTsconfig.indexOf(openFileRoot) >= 0) {
|
||||
this.reloadProjects();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getCanonicalFileName(fileName: string) {
|
||||
let name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase();
|
||||
return ts.normalizePath(name);
|
||||
}
|
||||
|
||||
watchedProjectConfigFileChanged(project: Project) {
|
||||
this.log("Config File Changed: " + project.projectFilename);
|
||||
this.log("Config file changed: " + project.projectFilename);
|
||||
this.updateConfiguredProject(project);
|
||||
this.updateProjectStructure();
|
||||
}
|
||||
@@ -567,11 +653,29 @@ namespace ts.server {
|
||||
}
|
||||
|
||||
createInferredProject(root: ScriptInfo) {
|
||||
var iproj = new Project(this);
|
||||
iproj.addRoot(root);
|
||||
iproj.finishGraph();
|
||||
this.inferredProjects.push(iproj);
|
||||
return iproj;
|
||||
var project = new Project(this);
|
||||
project.addRoot(root);
|
||||
|
||||
let currentPath = ts.getDirectoryPath(root.fileName);
|
||||
let parentPath = ts.getDirectoryPath(currentPath);
|
||||
while (currentPath != parentPath) {
|
||||
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();
|
||||
this.inferredProjects.push(project);
|
||||
return project;
|
||||
}
|
||||
|
||||
fileDeletedInFilesystem(info: ScriptInfo) {
|
||||
@@ -585,6 +689,9 @@ namespace ts.server {
|
||||
if (!info.isOpen) {
|
||||
this.filenameToScriptInfo[info.fileName] = undefined;
|
||||
var referencingProjects = this.findReferencingProjects(info);
|
||||
if (info.defaultProject) {
|
||||
info.defaultProject.removeRoot(info);
|
||||
}
|
||||
for (var i = 0, len = referencingProjects.length; i < len; i++) {
|
||||
referencingProjects[i].removeReferencedFile(info);
|
||||
}
|
||||
@@ -615,9 +722,24 @@ 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 the ref count for this directory watcher drops to 0, it's time to close it
|
||||
if (!(--project.projectService.directoryWatchersRefCount[directory])) {
|
||||
this.log("Close directory watcher for: " + directory);
|
||||
project.projectService.directoryWatchersForTsconfig[directory].close();
|
||||
delete project.projectService.directoryWatchersForTsconfig[directory];
|
||||
}
|
||||
}
|
||||
this.inferredProjects = copyListRemovingItem(project, this.inferredProjects);
|
||||
}
|
||||
|
||||
let fileNames = project.getFileNames();
|
||||
for (let fileName of fileNames) {
|
||||
@@ -659,8 +781,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
|
||||
@@ -683,6 +804,11 @@ namespace ts.server {
|
||||
* @param info The file that has been closed or newly configured
|
||||
*/
|
||||
closeOpenFile(info: ScriptInfo) {
|
||||
// Closing file should trigger re-reading the file content from disk. This is
|
||||
// because the user may chose to discard the buffer content before saving
|
||||
// to the disk, and the server's version of the file can be out of sync.
|
||||
info.svc.reloadFromFile(info.fileName);
|
||||
|
||||
var openFileRoots: ScriptInfo[] = [];
|
||||
var removedProject: Project;
|
||||
for (var i = 0, len = this.openFileRoots.length; i < len; i++) {
|
||||
@@ -713,19 +839,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);
|
||||
}
|
||||
@@ -769,7 +890,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);
|
||||
@@ -830,14 +955,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 && 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;
|
||||
@@ -922,6 +1058,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");
|
||||
@@ -1041,17 +1182,17 @@ namespace ts.server {
|
||||
// file references will be relative to dirPath (or absolute)
|
||||
var dirPath = ts.getDirectoryPath(configFilename);
|
||||
var contents = this.host.readFile(configFilename)
|
||||
var rawConfig: { config?: ProjectOptions; error?: Diagnostic; } = ts.parseConfigFileText(configFilename, contents);
|
||||
var rawConfig: { config?: ProjectOptions; error?: Diagnostic; } = ts.parseConfigFileTextToJson(configFilename, contents);
|
||||
if (rawConfig.error) {
|
||||
return { succeeded: false, error: rawConfig.error };
|
||||
}
|
||||
else {
|
||||
var parsedCommandLine = ts.parseConfigFile(rawConfig.config, this.host, dirPath);
|
||||
var parsedCommandLine = ts.parseJsonConfigFileContent(rawConfig.config, this.host, dirPath);
|
||||
if (parsedCommandLine.errors && (parsedCommandLine.errors.length > 0)) {
|
||||
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 = {
|
||||
@@ -1070,27 +1211,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);
|
||||
@@ -1105,7 +1251,9 @@ namespace ts.server {
|
||||
|
||||
for (let fileName of fileNamesToRemove) {
|
||||
let info = this.getScriptInfo(fileName);
|
||||
project.removeRoot(info);
|
||||
if (info) {
|
||||
project.removeRoot(info);
|
||||
}
|
||||
}
|
||||
|
||||
for (let fileName of fileNamesToAdd) {
|
||||
@@ -1217,9 +1365,9 @@ namespace ts.server {
|
||||
goSubtree: boolean;
|
||||
done: boolean;
|
||||
leaf(relativeStart: number, relativeLength: number, lineCollection: LineLeaf): void;
|
||||
pre? (relativeStart: number, relativeLength: number, lineCollection: LineCollection,
|
||||
pre?(relativeStart: number, relativeLength: number, lineCollection: LineCollection,
|
||||
parent: LineNode, nodeType: CharRangeSection): LineCollection;
|
||||
post? (relativeStart: number, relativeLength: number, lineCollection: LineCollection,
|
||||
post?(relativeStart: number, relativeLength: number, lineCollection: LineCollection,
|
||||
parent: LineNode, nodeType: CharRangeSection): LineCollection;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace ts.server {
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
terminal: false,
|
||||
});
|
||||
});
|
||||
|
||||
class Logger implements ts.server.Logger {
|
||||
fd = -1;
|
||||
@@ -58,7 +58,7 @@ namespace ts.server {
|
||||
isVerbose() {
|
||||
return this.loggingEnabled() && (this.level == "verbose");
|
||||
}
|
||||
|
||||
|
||||
|
||||
msg(s: string, type = "Err") {
|
||||
if (this.fd < 0) {
|
||||
@@ -83,95 +83,6 @@ namespace ts.server {
|
||||
}
|
||||
}
|
||||
|
||||
interface WatchedFile {
|
||||
fileName: string;
|
||||
callback: (fileName: string, removed: boolean) => void;
|
||||
mtime: Date;
|
||||
}
|
||||
|
||||
class WatchedFileSet {
|
||||
private watchedFiles: WatchedFile[] = [];
|
||||
private nextFileToCheck = 0;
|
||||
private watchTimer: NodeJS.Timer;
|
||||
|
||||
// average async stat takes about 30 microseconds
|
||||
// set chunk size to do 30 files in < 1 millisecond
|
||||
constructor(public interval = 2500, public chunkSize = 30) {
|
||||
}
|
||||
|
||||
private static copyListRemovingItem<T>(item: T, list: T[]) {
|
||||
var copiedList: T[] = [];
|
||||
for (var i = 0, len = list.length; i < len; i++) {
|
||||
if (list[i] != item) {
|
||||
copiedList.push(list[i]);
|
||||
}
|
||||
}
|
||||
return copiedList;
|
||||
}
|
||||
|
||||
private static getModifiedTime(fileName: string): Date {
|
||||
return fs.statSync(fileName).mtime;
|
||||
}
|
||||
|
||||
private poll(checkedIndex: number) {
|
||||
var watchedFile = this.watchedFiles[checkedIndex];
|
||||
if (!watchedFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.stat(watchedFile.fileName,(err, stats) => {
|
||||
if (err) {
|
||||
watchedFile.callback(watchedFile.fileName, /* removed */ false);
|
||||
}
|
||||
else if (watchedFile.mtime.getTime() !== stats.mtime.getTime()) {
|
||||
watchedFile.mtime = WatchedFileSet.getModifiedTime(watchedFile.fileName);
|
||||
watchedFile.callback(watchedFile.fileName, watchedFile.mtime.getTime() === 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// this implementation uses polling and
|
||||
// stat due to inconsistencies of fs.watch
|
||||
// and efficiency of stat on modern filesystems
|
||||
private startWatchTimer() {
|
||||
this.watchTimer = setInterval(() => {
|
||||
var count = 0;
|
||||
var nextToCheck = this.nextFileToCheck;
|
||||
var firstCheck = -1;
|
||||
while ((count < this.chunkSize) && (nextToCheck !== firstCheck)) {
|
||||
this.poll(nextToCheck);
|
||||
if (firstCheck < 0) {
|
||||
firstCheck = nextToCheck;
|
||||
}
|
||||
nextToCheck++;
|
||||
if (nextToCheck === this.watchedFiles.length) {
|
||||
nextToCheck = 0;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
this.nextFileToCheck = nextToCheck;
|
||||
}, this.interval);
|
||||
}
|
||||
|
||||
addFile(fileName: string, callback: (fileName: string, removed: boolean) => void ): WatchedFile {
|
||||
var file: WatchedFile = {
|
||||
fileName,
|
||||
callback,
|
||||
mtime: WatchedFileSet.getModifiedTime(fileName)
|
||||
};
|
||||
|
||||
this.watchedFiles.push(file);
|
||||
if (this.watchedFiles.length === 1) {
|
||||
this.startWatchTimer();
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
removeFile(file: WatchedFile) {
|
||||
this.watchedFiles = WatchedFileSet.copyListRemovingItem(file, this.watchedFiles);
|
||||
}
|
||||
}
|
||||
|
||||
class IOSession extends Session {
|
||||
constructor(host: ServerHost, logger: ts.server.Logger) {
|
||||
super(host, Buffer.byteLength, process.hrtime, logger);
|
||||
@@ -244,31 +155,10 @@ namespace ts.server {
|
||||
|
||||
var logger = createLoggerFromEnv();
|
||||
|
||||
// REVIEW: for now this implementation uses polling.
|
||||
// The advantage of polling is that it works reliably
|
||||
// on all os and with network mounted files.
|
||||
// For 90 referenced files, the average time to detect
|
||||
// changes is 2*msInterval (by default 5 seconds).
|
||||
// The overhead of this is .04 percent (1/2500) with
|
||||
// average pause of < 1 millisecond (and max
|
||||
// pause less than 1.5 milliseconds); question is
|
||||
// do we anticipate reference sets in the 100s and
|
||||
// do we care about waiting 10-20 seconds to detect
|
||||
// changes for large reference sets? If so, do we want
|
||||
// to increase the chunk size or decrease the interval
|
||||
// time dynamically to match the large reference set?
|
||||
var watchedFileSet = new WatchedFileSet();
|
||||
ts.sys.watchFile = function (fileName, callback) {
|
||||
var watchedFile = watchedFileSet.addFile(fileName, callback);
|
||||
return {
|
||||
close: () => watchedFileSet.removeFile(watchedFile)
|
||||
}
|
||||
|
||||
};
|
||||
var ioSession = new IOSession(ts.sys, logger);
|
||||
process.on('uncaughtException', function(err: Error) {
|
||||
ioSession.logError(err, "unknown");
|
||||
});
|
||||
// Start listening
|
||||
ioSession.listen();
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,21 @@ namespace ts.server {
|
||||
return spaceCache[n];
|
||||
}
|
||||
|
||||
export function generateIndentString(n: number, editorOptions: EditorOptions): string {
|
||||
if (editorOptions.ConvertTabsToSpaces) {
|
||||
return generateSpaces(n);
|
||||
} else {
|
||||
var result = "";
|
||||
for (var i = 0; i < Math.floor(n / editorOptions.TabSize); i++) {
|
||||
result += "\t";
|
||||
}
|
||||
for (var i = 0; i < n % editorOptions.TabSize; i++) {
|
||||
result += " ";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
interface FileStart {
|
||||
file: string;
|
||||
start: ILineInfo;
|
||||
@@ -608,27 +623,25 @@ namespace ts.server {
|
||||
ConvertTabsToSpaces: formatOptions.ConvertTabsToSpaces,
|
||||
IndentStyle: ts.IndentStyle.Smart,
|
||||
};
|
||||
var indentPosition =
|
||||
compilerService.languageService.getIndentationAtPosition(file, position, editorOptions);
|
||||
var preferredIndent = compilerService.languageService.getIndentationAtPosition(file, position, editorOptions);
|
||||
var hasIndent = 0;
|
||||
for (var i = 0, len = lineText.length; i < len; i++) {
|
||||
if (lineText.charAt(i) == " ") {
|
||||
indentPosition--;
|
||||
hasIndent++;
|
||||
}
|
||||
else if (lineText.charAt(i) == "\t") {
|
||||
indentPosition -= editorOptions.IndentSize;
|
||||
hasIndent += editorOptions.TabSize;
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (indentPosition > 0) {
|
||||
var spaces = generateSpaces(indentPosition);
|
||||
edits.push({ span: ts.createTextSpanFromBounds(position, position), newText: spaces });
|
||||
}
|
||||
else if (indentPosition < 0) {
|
||||
// i points to the first non whitespace character
|
||||
if (preferredIndent !== hasIndent) {
|
||||
var firstNoWhiteSpacePosition = lineInfo.offset + i;
|
||||
edits.push({
|
||||
span: ts.createTextSpanFromBounds(position, position - indentPosition),
|
||||
newText: ""
|
||||
span: ts.createTextSpanFromBounds(lineInfo.offset, firstNoWhiteSpacePosition),
|
||||
newText: generateIndentString(preferredIndent, editorOptions)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -780,6 +793,7 @@ namespace ts.server {
|
||||
}
|
||||
|
||||
private closeClientFile(fileName: string) {
|
||||
if (!fileName) { return; }
|
||||
var file = ts.normalizePath(fileName);
|
||||
this.projectService.closeClientFile(file);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user