Merge branch 'master' into tscJsFiles

This commit is contained in:
Sheetal Nandi
2015-10-15 12:30:55 -07:00
260 changed files with 14463 additions and 678 deletions

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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);
}