[in progress] project system work - major code reorg

This commit is contained in:
Vladimir Matveev 2016-06-24 14:30:45 -07:00
parent cf616dc292
commit cefaa171eb
7 changed files with 386 additions and 330 deletions

View File

@ -8,7 +8,6 @@
/// <reference path="project.ts"/>
namespace ts.server {
export const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024;
/**
@ -24,10 +23,29 @@ namespace ts.server {
}
export interface HostConfiguration {
formatCodeOptions: ts.FormatCodeSettings;
formatCodeOptions: FormatCodeSettings;
hostInfo: string;
}
interface ConfigFileConversionResult {
success: boolean;
errors?: Diagnostic[];
projectOptions?: ProjectOptions;
}
interface OpenConfigFileResult {
success: boolean,
errors?: Diagnostic[]
project?: ConfiguredProject,
}
interface OpenConfiguredProjectResult {
configFileName?: string;
configFileErrors?: Diagnostic[];
}
function findProjectByName<T extends Project>(projectName: string, projects: T[]): T {
for (const proj of projects) {
if (proj.getProjectName() === projectName) {
@ -36,22 +54,16 @@ namespace ts.server {
}
}
export interface ProjectOpenResult {
success?: boolean;
errorMsg?: string;
project?: Project;
}
class DirectoryWatchers {
/**
* a path to directory watcher map that detects added tsconfig files
**/
private directoryWatchersForTsconfig: ts.Map<FileWatcher> = {};
private directoryWatchersForTsconfig: 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.
**/
private directoryWatchersRefCount: ts.Map<number> = {};
private directoryWatchersRefCount: Map<number> = {};
constructor(private readonly projectService: ProjectService) {
}
@ -60,18 +72,18 @@ namespace ts.server {
// if the ref count for this directory watcher drops to 0, it's time to close it
this.directoryWatchersRefCount[directory]--;
if (this.directoryWatchersRefCount[directory] === 0) {
this.projectService.log("Close directory watcher for: " + directory);
this.projectService.log(`Close directory watcher for: ${directory}`);
this.directoryWatchersForTsconfig[directory].close();
delete this.directoryWatchersForTsconfig[directory];
}
}
startWatchingContainingDirectoriesForFile(fileName: string, project: InferredProject, callback: (fileName: string) => void) {
let currentPath = ts.getDirectoryPath(fileName);
let parentPath = ts.getDirectoryPath(currentPath);
let currentPath = getDirectoryPath(fileName);
let parentPath = getDirectoryPath(currentPath);
while (currentPath != parentPath) {
if (!this.directoryWatchersForTsconfig[currentPath]) {
this.projectService.log("Add watcher for: " + currentPath);
this.projectService.log(`Add watcher for: ${currentPath}`);
this.directoryWatchersForTsconfig[currentPath] = this.projectService.host.watchDirectory(currentPath, callback);
this.directoryWatchersRefCount[currentPath] = 1;
}
@ -80,30 +92,32 @@ namespace ts.server {
}
project.directoriesWatchedForTsconfig.push(currentPath);
currentPath = parentPath;
parentPath = ts.getDirectoryPath(parentPath);
parentPath = getDirectoryPath(parentPath);
}
}
}
export class ProjectService {
private readonly documentRegistry: DocumentRegistry;
/**
* Container of all known scripts
*/
private filenameToScriptInfo = createNormalizedPathMap<ScriptInfo>();
private readonly filenameToScriptInfo = createNormalizedPathMap<ScriptInfo>();
/**
* maps external project file name to list of config files that were the part of this project
*/
externalProjectToConfiguredProjectMap: Map<NormalizedPath[]>;
private readonly externalProjectToConfiguredProjectMap: Map<NormalizedPath[]>;
/**
* external projects (configuration and list of root files is not controlled by tsserver)
*/
externalProjects: ExternalProject[] = [];
readonly externalProjects: ExternalProject[] = [];
/**
* projects built from openFileRoots
**/
inferredProjects: InferredProject[] = [];
readonly inferredProjects: InferredProject[] = [];
/**
* projects specified by a tsconfig.json file
**/
@ -121,22 +135,21 @@ namespace ts.server {
**/
openFileRootsConfigured: ScriptInfo[] = [];
private directoryWatchers: DirectoryWatchers;
private readonly directoryWatchers: DirectoryWatchers;
private hostConfiguration: HostConfiguration;
private timerForDetectingProjectFileListChanges: Map<any> = {};
private documentRegistry: ts.DocumentRegistry;
constructor(public readonly host: ServerHost,
public readonly psLogger: Logger,
public readonly logger: Logger,
public readonly cancellationToken: HostCancellationToken,
private readonly eventHandler?: ProjectServiceEventHandler) {
this.directoryWatchers = new DirectoryWatchers(this);
// ts.disableIncrementalParsing = true;
this.setDefaultHostConfiguration();
this.documentRegistry = ts.createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory());
this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory());
}
private setDefaultHostConfiguration() {
@ -162,7 +175,7 @@ namespace ts.server {
getFormatCodeOptions(file?: NormalizedPath) {
if (file) {
const info = this.filenameToScriptInfo.get(file);
const info = this.getScriptInfoForNormalizedPath(file);
if (info) {
return info.formatCodeSettings;
}
@ -171,9 +184,9 @@ namespace ts.server {
}
private onSourceFileChanged(fileName: NormalizedPath) {
const info = this.filenameToScriptInfo.get(fileName);
const info = this.getScriptInfoForNormalizedPath(fileName);
if (!info) {
this.psLogger.info("Error: got watch notification for unknown file: " + fileName);
this.logger.info(`Error: got watch notification for unknown file: ${fileName}`);
}
if (!this.host.fileExists(fileName)) {
@ -182,13 +195,13 @@ namespace ts.server {
}
else {
if (info && (!info.isOpen)) {
info.reloadFromFile(info.fileName);
info.reloadFromFile();
}
}
}
private handleDeletedFile(info: ScriptInfo) {
this.psLogger.info(info.fileName + " deleted");
this.logger.info(`${info.fileName} deleted`);
info.stopWatcher();
@ -212,6 +225,7 @@ namespace ts.server {
this.printProjects();
}
/**
* 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
@ -225,7 +239,7 @@ namespace ts.server {
return;
}
this.log("Detected source file changes: " + fileName);
this.log(`Detected source file changes: ${fileName}`);
const timeoutId = this.timerForDetectingProjectFileListChanges[project.configFileName];
if (timeoutId) {
this.host.clearTimeout(timeoutId);
@ -237,13 +251,13 @@ namespace ts.server {
}
private handleChangeInSourceFileForConfiguredProject(project: ConfiguredProject) {
const { projectOptions } = this.configFileToProjectOptions(project.configFileName);
const { projectOptions } = this.convertConfigFileContentToProjectOptions(project.configFileName);
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 && currentRootFiles.sort(), newRootFiles && newRootFiles.sort())) {
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.
@ -256,7 +270,7 @@ namespace ts.server {
}
private onConfigChangedForConfiguredProject(project: ConfiguredProject) {
this.log("Config file changed: " + project.configFileName);
this.log(`Config file changed: ${project.configFileName}`);
this.updateConfiguredProject(project);
this.updateProjectStructure();
}
@ -264,39 +278,38 @@ namespace ts.server {
/**
* This is the callback function when a watched directory has an added tsconfig file.
*/
private onConfigChangeForInferredProject(fileName: string) {
if (ts.getBaseFileName(fileName) != "tsconfig.json") {
this.log(fileName + " is not tsconfig.json");
private onConfigFileAddedForInferredProject(fileName: string) {
// TODO: check directory separators
if (getBaseFileName(fileName) != "tsconfig.json") {
this.log(`${fileName} is not tsconfig.json`);
return;
}
this.log("Detected newly added tsconfig file: " + fileName);
const { projectOptions } = this.configFileToProjectOptions(fileName);
this.log(`Detected newly added tsconfig file: ${fileName}`);
const { projectOptions } = this.convertConfigFileContentToProjectOptions(fileName);
const rootFilesInTsconfig = projectOptions.files.map(f => this.getCanonicalFileName(f));
// We should only care about the new tsconfig file if it contains any
// opened root files of existing inferred projects
for (const rootFile of this.openFileRoots) {
if (contains(rootFilesInTsconfig, this.getCanonicalFileName(rootFile.fileName))) {
this.reloadProjects();
return;
break;
}
}
}
private getCanonicalFileName(fileName: string) {
const name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase();
return ts.normalizePath(name);
return normalizePath(name);
}
// TODO: delete if unused
private releaseNonReferencedConfiguredProjects() {
if (this.configuredProjects.every(p => p.openRefCount > 0)) {
return;
}
const configuredProjects: ConfiguredProject[] = [];
for (const proj of this.configuredProjects) {
if (proj.openRefCount > 0) {
configuredProjects.push(proj);
@ -310,7 +323,7 @@ namespace ts.server {
}
private removeProject(project: Project) {
this.log("remove project: " + project.getRootFiles().toString());
this.log(`remove project: ${project.getRootFiles().toString()}`);
project.close();
@ -345,14 +358,15 @@ namespace ts.server {
return undefined;
}
private addOpenFile(info: ScriptInfo) {
private addOpenFile(info: ScriptInfo): void {
const externalProject = this.findContainingExternalProject(info.fileName);
if (externalProject) {
// file is already included in some external project - do nothing
return;
}
const configuredProject = this.findContainingConfiguredProject(info);
if (configuredProject) {
// info.defaultProject = configuredProject;
// file is the part of configured project
configuredProject.addOpenRef();
if (configuredProject.isRoot(info)) {
this.openFileRootsConfigured.push(info);
@ -362,6 +376,7 @@ namespace ts.server {
}
return;
}
// create new inferred project p with the newly opened file as root
const inferredProject = this.createAndAddInferredProject(info);
const openFileRoots: ScriptInfo[] = [];
@ -369,8 +384,11 @@ namespace ts.server {
for (const rootFile of this.openFileRoots) {
// if r referenced by the new project
if (inferredProject.containsScriptInfo(rootFile)) {
// remove project rooted at r
this.removeProject(rootFile.getDefaultProject());
// remove inferred project that was initially created for rootFile
const defaultProject = rootFile.getDefaultProject();
Debug.assert(defaultProject.projectKind === ProjectKind.Inferred);
this.removeProject(defaultProject);
// put r in referenced open file list
this.openFilesReferenced.push(rootFile);
// set default project of r to the new project
@ -389,14 +407,14 @@ namespace ts.server {
* Remove this file from the set of open, non-configured files.
* @param info The file that has been closed or newly configured
*/
private closeOpenFile(info: ScriptInfo) {
private closeOpenFile(info: ScriptInfo): void {
// 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.reloadFromFile(info.fileName);
info.reloadFromFile();
this.openFileRoots = copyListRemovingItem(info, this.openFileRoots);
this.openFileRootsConfigured = copyListRemovingItem(info, this.openFileRootsConfigured);
removeItemFromSet(this.openFileRoots, info);
removeItemFromSet(this.openFileRootsConfigured, info);
// collect all projects that should be removed
let projectsToRemove: Project[];
@ -421,6 +439,10 @@ namespace ts.server {
const orphanFiles: ScriptInfo[] = [];
// for all open, referenced files f
for (const f of this.openFilesReferenced) {
if (f === info) {
// skip closed file
continue;
}
// collect orphanted files and try to re-add them as newly opened
if (f.containingProjects.length === 0) {
orphanFiles.push(f);
@ -430,17 +452,20 @@ namespace ts.server {
openFilesReferenced.push(f);
}
}
this.openFilesReferenced = openFilesReferenced;
// treat orphaned files as newly opened
for (let i = 0, len = orphanFiles.length; i < len; i++) {
this.addOpenFile(orphanFiles[i]);
for (const f of orphanFiles) {
this.addOpenFile(f);
}
}
else {
this.openFilesReferenced = copyListRemovingItem(info, this.openFilesReferenced);
// just close file
removeItemFromSet(this.openFilesReferenced, info);
}
this.releaseNonReferencedConfiguredProjects();
// projectsToRemove should already cover it
// this.releaseNonReferencedConfiguredProjects();
info.isOpen = false;
}
@ -450,36 +475,38 @@ namespace ts.server {
* 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.
*/
private openOrUpdateConfiguredProjectForFile(fileName: string): { configFileName?: string, configFileErrors?: Diagnostic[] } {
const searchPath = asNormalizedPath(getDirectoryPath(toNormalizedPath(fileName)));
this.log("Search path: " + searchPath, "Info");
private openOrUpdateConfiguredProjectForFile(fileName: NormalizedPath): OpenConfiguredProjectResult {
const searchPath = getDirectoryPath(fileName);
this.log(`Search path: ${searchPath}`, "Info");
// check if this file is already included in one of external projects
const configFileName = this.findConfigFile(searchPath);
if (configFileName) {
this.log("Config file name: " + configFileName, "Info");
const project = this.findConfiguredProjectByProjectName(configFileName);
if (!project) {
const { success, errors } = this.openConfigFile(configFileName, fileName);
if (!success) {
return { configFileName, configFileErrors: errors };
}
else {
// even if opening config file was successful, it could still
// contain errors that were tolerated.
this.log("Opened configuration file " + configFileName, "Info");
if (errors && errors.length > 0) {
return { configFileName, configFileErrors: errors };
}
}
const configFileName = this.findConfigFile(asNormalizedPath(searchPath));
if (!configFileName) {
this.log("No config files found.");
return {};
}
this.log(`Config file name: ${configFileName}`, "Info");
const project = this.findConfiguredProjectByProjectName(configFileName);
if (!project) {
const { success, errors } = this.openConfigFile(configFileName, fileName);
if (!success) {
return { configFileName, configFileErrors: errors };
}
else {
this.updateConfiguredProject(project);
// even if opening config file was successful, it could still
// contain errors that were tolerated.
this.log(`Opened configuration file ${configFileName}`, "Info");
if (errors && errors.length > 0) {
return { configFileName, configFileErrors: errors };
}
}
else {
this.log("No config files found.");
this.updateConfiguredProject(project);
}
return configFileName ? { configFileName } : {};
return { configFileName };
}
// This is different from the method the compiler uses because
@ -509,38 +536,42 @@ namespace ts.server {
}
private printProjects() {
if (!this.psLogger.isVerbose()) {
if (!this.logger.isVerbose()) {
return;
}
this.psLogger.startGroup();
for (let i = 0, len = this.inferredProjects.length; i < len; i++) {
const project = this.inferredProjects[i];
project.updateGraph();
this.psLogger.info("Project " + i.toString());
this.psLogger.info(project.filesToString());
this.psLogger.info("-----------------------------------------------");
this.logger.startGroup();
let counter = 0;
counter = printProjects(this.externalProjects, counter);
counter = printProjects(this.configuredProjects, counter);
counter = printProjects(this.inferredProjects, counter);
this.logger.info("Open file roots of inferred projects: ");
for (const rootFile of this.openFileRoots) {
this.logger.info(rootFile.fileName);
}
for (let i = 0, len = this.configuredProjects.length; i < len; i++) {
const project = this.configuredProjects[i];
project.updateGraph();
this.psLogger.info("Project (configured) " + (i + this.inferredProjects.length).toString());
this.psLogger.info(project.filesToString());
this.psLogger.info("-----------------------------------------------");
}
this.psLogger.info("Open file roots of inferred projects: ");
for (let i = 0, len = this.openFileRoots.length; i < len; i++) {
this.psLogger.info(this.openFileRoots[i].fileName);
}
this.psLogger.info("Open files referenced by inferred or configured projects: ");
this.logger.info("Open files referenced by inferred or configured projects: ");
for (const referencedFile of this.openFilesReferenced) {
const fileInfo = `${referencedFile.fileName} ${ProjectKind[referencedFile.getDefaultProject().projectKind]}`;
this.psLogger.info(fileInfo);
this.logger.info(fileInfo);
}
this.psLogger.info("Open file roots of configured projects: ");
for (let i = 0, len = this.openFileRootsConfigured.length; i < len; i++) {
this.psLogger.info(this.openFileRootsConfigured[i].fileName);
this.logger.info("Open file roots of configured projects: ");
for (const configuredRoot of this.openFileRootsConfigured) {
this.logger.info(configuredRoot.fileName);
}
this.logger.endGroup();
function printProjects(projects: Project[], counter: number) {
for (const project of projects) {
project.updateGraph();
this.psLogger.info(`Project '${project.getProjectName()}' (${ProjectKind[project.projectKind]}) ${counter}`);
this.psLogger.info(project.filesToString());
this.psLogger.info("-----------------------------------------------");
counter++;
}
return counter;
}
this.psLogger.endGroup();
}
private findConfiguredProjectByProjectName(configFileName: NormalizedPath) {
@ -551,48 +582,46 @@ namespace ts.server {
return findProjectByName(projectFileName, this.externalProjects);
}
private configFileToProjectOptions(configFilename: string): { succeeded: boolean, projectOptions?: ProjectOptions, errors?: Diagnostic[] } {
configFilename = ts.normalizePath(configFilename);
// file references will be relative to dirPath (or absolute)
const dirPath = ts.getDirectoryPath(configFilename);
const contents = this.host.readFile(configFilename);
const rawConfig: { config?: ProjectOptions; error?: Diagnostic; } = ts.parseConfigFileTextToJson(configFilename, contents);
if (rawConfig.error) {
return { succeeded: false, errors: [rawConfig.error] };
}
else {
const configHasFilesProperty = rawConfig.config["files"] !== undefined;
const parsedCommandLine = ts.parseJsonConfigFileContent(rawConfig.config, this.host, dirPath, /*existingOptions*/ {}, configFilename);
Debug.assert(!!parsedCommandLine.fileNames);
private convertConfigFileContentToProjectOptions(configFilename: string): ConfigFileConversionResult {
configFilename = normalizePath(configFilename);
if (parsedCommandLine.errors && (parsedCommandLine.errors.length > 0)) {
return { succeeded: false, errors: parsedCommandLine.errors };
}
else if (parsedCommandLine.fileNames.length === 0) {
const error = createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename);
return { succeeded: false, errors: [error] };
}
else {
const projectOptions: ProjectOptions = {
files: parsedCommandLine.fileNames,
compilerOptions: parsedCommandLine.options,
configHasFilesProperty,
wildcardDirectories: parsedCommandLine.wildcardDirectories,
};
return { succeeded: true, projectOptions };
}
const configObj = parseConfigFileTextToJson(configFilename, this.host.readFile(configFilename));
if (configObj.error) {
return { success: false, errors: [configObj.error] };
}
const parsedCommandLine = parseJsonConfigFileContent(
configObj.config,
this.host,
getDirectoryPath(configFilename),
/*existingOptions*/ {},
configFilename);
Debug.assert(!!parsedCommandLine.fileNames);
if (parsedCommandLine.errors && (parsedCommandLine.errors.length > 0)) {
return { success: false, errors: parsedCommandLine.errors };
}
if (parsedCommandLine.fileNames.length === 0) {
const error = createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename);
return { success: false, errors: [error] };
}
const projectOptions: ProjectOptions = {
files: parsedCommandLine.fileNames,
compilerOptions: parsedCommandLine.options,
configHasFilesProperty: configObj.config["files"] !== undefined,
wildcardDirectories: parsedCommandLine.wildcardDirectories,
};
return { success: true, projectOptions };
}
private exceedTotalNonTsFileSizeLimit(options: CompilerOptions, fileNames: string[]) {
if (options && options.disableSizeLimit) {
private exceededTotalSizeLimitForNonTsFiles(options: CompilerOptions, fileNames: string[]) {
if (options && options.disableSizeLimit || !this.host.getFileSize) {
return false;
}
let totalNonTsFileSize = 0;
if (!this.host.getFileSize) {
return false;
}
for (const fileName of fileNames) {
if (hasTypeScriptFileExtension(fileName)) {
continue;
@ -605,16 +634,21 @@ namespace ts.server {
return false;
}
private createAndAddExternalProject(projectFileName: string, files: string[], compilerOptions: CompilerOptions, clientFileName?: string) {
const sizeLimitExceeded = this.exceedTotalNonTsFileSizeLimit(compilerOptions, files);
const project = new ExternalProject(projectFileName, this, this.documentRegistry, compilerOptions, !sizeLimitExceeded);
const errors = this.addFilesToProject(project, files, clientFileName);
private createAndAddExternalProject(projectFileName: string, files: string[], compilerOptions: CompilerOptions) {
const project = new ExternalProject(
projectFileName,
this,
this.documentRegistry,
compilerOptions,
/*languageServiceEnabled*/ !this.exceededTotalSizeLimitForNonTsFiles(compilerOptions, files));
const errors = this.addFilesToProjectAndUpdateGraph(project, files, /*clientFileName*/ undefined);
this.externalProjects.push(project);
return { project, errors };
}
private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, clientFileName?: string) {
const sizeLimitExceeded = this.exceedTotalNonTsFileSizeLimit(projectOptions.compilerOptions, projectOptions.files);
const sizeLimitExceeded = !this.exceededTotalSizeLimitForNonTsFiles(projectOptions.compilerOptions, projectOptions.files);
const project = new ConfiguredProject(
configFileName,
this,
@ -622,25 +656,27 @@ namespace ts.server {
projectOptions.configHasFilesProperty,
projectOptions.compilerOptions,
projectOptions.wildcardDirectories,
!sizeLimitExceeded);
/*languageServiceEnabled*/ !sizeLimitExceeded);
const errors = this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, clientFileName);
const errors = this.addFilesToProject(project, projectOptions.files, clientFileName);
project.watchConfigFile(project => this.onConfigChangedForConfiguredProject(project));
if (!sizeLimitExceeded) {
this.watchConfigDirectoryForProject(project, projectOptions);
}
project.watchWildcards((project, path) => this.onSourceFileInDirectoryChangedForConfiguredProject(project, path));
this.configuredProjects.push(project);
return { project, errors };
}
private watchConfigDirectoryForProject(project: ConfiguredProject, options: ProjectOptions) {
private watchConfigDirectoryForProject(project: ConfiguredProject, options: ProjectOptions): void {
if (!options.configHasFilesProperty) {
project.watchConfigDirectory((project, path) => this.onSourceFileInDirectoryChangedForConfiguredProject(project, path));
}
}
private addFilesToProject(project: ConfiguredProject | ExternalProject, files: string[], clientFileName: string): Diagnostic[] {
private addFilesToProjectAndUpdateGraph(project: ConfiguredProject | ExternalProject, files: string[], clientFileName: string): Diagnostic[] {
let errors: Diagnostic[];
for (const rootFilename of files) {
if (this.host.fileExists(rootFilename)) {
@ -655,22 +691,23 @@ namespace ts.server {
return errors;
}
private openConfigFile(configFileName: NormalizedPath, clientFileName?: string): { success: boolean, project?: ConfiguredProject, errors?: Diagnostic[] } {
const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(configFileName);
if (!succeeded) {
return { success: false, errors };
}
else {
const { project, errors } = this.createAndAddConfiguredProject(configFileName, projectOptions, clientFileName);
return { success: true, project, errors };
private openConfigFile(configFileName: NormalizedPath, clientFileName?: string): OpenConfigFileResult {
const conversionResult = this.convertConfigFileContentToProjectOptions(configFileName);
if (!conversionResult.success) {
return { success: false, errors: conversionResult.errors };
}
const { project, errors } = this.createAndAddConfiguredProject(configFileName, conversionResult.projectOptions, clientFileName);
return { success: true, project, errors };
}
private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newRootFiles: string[], newOptions: CompilerOptions) {
const oldRootFiles = project.getRootFiles();
// TODO: verify that newRootFiles are always normalized
// TODO: avoid N^2
const newFileNames = asNormalizedPathArray(filter(newRootFiles, f => this.host.fileExists(f)));
const fileNamesToRemove = oldRootFiles.filter(f => !contains(newFileNames, f));
const fileNamesToAdd = newFileNames.filter(f => !contains(oldRootFiles, f));
const fileNamesToRemove = asNormalizedPathArray(oldRootFiles.filter(f => !contains(newFileNames, f)));
const fileNamesToAdd = asNormalizedPathArray(newFileNames.filter(f => !contains(oldRootFiles, f)));
for (const fileName of fileNamesToRemove) {
const info = this.getScriptInfoForNormalizedPath(fileName);
@ -680,7 +717,7 @@ namespace ts.server {
}
for (const fileName of fileNamesToAdd) {
let info = this.getScriptInfo(fileName);
let info = this.getScriptInfoForNormalizedPath(fileName);
if (!info) {
info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false);
}
@ -689,7 +726,8 @@ namespace ts.server {
// openFileRoots or openFileReferenced.
if (info.isOpen) {
if (contains(this.openFileRoots, info)) {
this.openFileRoots = copyListRemovingItem(info, this.openFileRoots);
removeItemFromSet(this.openFileRoots, info);
// delete inferred project
let toRemove: Project[];
for (const p of info.containingProjects) {
@ -722,30 +760,29 @@ namespace ts.server {
if (!this.host.fileExists(project.configFileName)) {
this.log("Config file deleted");
this.removeProject(project);
return;
}
const { success, projectOptions, errors } = this.convertConfigFileContentToProjectOptions(project.configFileName);
if (!success) {
return errors;
}
if (this.exceededTotalSizeLimitForNonTsFiles(projectOptions.compilerOptions, projectOptions.files)) {
project.setCompilerOptions(projectOptions.compilerOptions);
if (!project.languageServiceEnabled) {
// language service is already disabled
return;
}
project.disableLanguageService();
project.stopWatchingDirectory();
}
else {
const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(project.configFileName);
if (!succeeded) {
return errors;
}
else {
if (this.exceedTotalNonTsFileSizeLimit(projectOptions.compilerOptions, projectOptions.files)) {
project.setCompilerOptions(projectOptions.compilerOptions);
if (!project.languageServiceEnabled) {
// language service is already disabled
return;
}
project.disableLanguageService();
project.stopWatchingDirectory();
}
else {
if (!project.languageServiceEnabled) {
project.enableLanguageService();
}
this.watchConfigDirectoryForProject(project, projectOptions);
this.updateNonInferredProject(project, projectOptions.files, projectOptions.compilerOptions);
}
if (!project.languageServiceEnabled) {
project.enableLanguageService();
}
this.watchConfigDirectoryForProject(project, projectOptions);
this.updateNonInferredProject(project, projectOptions.files, projectOptions.compilerOptions);
}
}
@ -756,7 +793,7 @@ namespace ts.server {
this.directoryWatchers.startWatchingContainingDirectoriesForFile(
root.fileName,
project,
fileName => this.onConfigChangeForInferredProject(fileName));
fileName => this.onConfigFileAddedForInferredProject(fileName));
project.updateGraph();
this.inferredProjects.push(project);
@ -764,15 +801,20 @@ namespace ts.server {
}
/**
* @param filename is absolute pathname
* @param uncheckedFileName is absolute pathname
* @param fileContent is a known version of the file content that is more up to date than the one on disk
*/
getOrCreateScriptInfo(fileName: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) {
return this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(fileName), openedByClient, fileContent, scriptKind);
getOrCreateScriptInfo(uncheckedFileName: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) {
return this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName), openedByClient, fileContent, scriptKind);
}
getScriptInfo(uncheckedFileName: string) {
return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName));
}
getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) {
let info = this.filenameToScriptInfo.get(fileName);
let info = this.getScriptInfoForNormalizedPath(fileName);
if (!info) {
let content: string;
if (this.host.fileExists(fileName)) {
@ -803,22 +845,26 @@ namespace ts.server {
return info;
}
log(msg: string, type = "Err") {
this.psLogger.msg(msg, type);
getScriptInfoForNormalizedPath(fileName: NormalizedPath) {
return this.filenameToScriptInfo.get(fileName);
}
setHostConfiguration(args: ts.server.protocol.ConfigureRequestArguments) {
log(msg: string, type = "Err") {
this.logger.msg(msg, type);
}
setHostConfiguration(args: protocol.ConfigureRequestArguments) {
if (args.file) {
const info = this.filenameToScriptInfo.get(toNormalizedPath(args.file));
const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(args.file));
if (info) {
info.setFormatOptions(args.formatOptions);
this.log("Host configuration update for file " + args.file, "Info");
this.log(`Host configuration update for file ${args.file}`, "Info");
}
}
else {
if (args.hostInfo !== undefined) {
this.hostConfiguration.hostInfo = args.hostInfo;
this.log("Host information " + args.hostInfo, "Info");
this.log(`Host information ${args.hostInfo}`, "Info");
}
if (args.formatOptions) {
mergeMaps(this.hostConfiguration.formatCodeOptions, args.formatOptions);
@ -828,7 +874,7 @@ namespace ts.server {
}
closeLog() {
this.psLogger.close();
this.logger.close();
}
/**
@ -921,27 +967,33 @@ namespace ts.server {
}
}
else {
//
openFileRoots.push(rootFile);
}
if (rootFile.containingProjects.some(p => p.projectKind !== ProjectKind.Inferred)) {
// file was included in non-inferred project - drop old inferred project
}
else {
openFileRoots.push(rootFile);
}
let inferredProjectsToRemove: Project[];
for (const p of rootFile.containingProjects) {
if (p.projectKind !== ProjectKind.Inferred) {
// file was included in non-inferred project - drop old inferred project
if (rootFile.containingProjects.length === 1) {
// file contained only in one project
openFileRoots.push(rootFile);
}
else {
// TODO: fixme
// file is contained in more than one inferred project - keep only ones where it is used as reference
const roots = rootFile.containingProjects.filter(p => p.isRoot(rootFile));
for (const root of roots) {
this.removeProject(root);
}
Debug.assert(rootFile.containingProjects.length > 0);
this.openFilesReferenced.push(rootFile);
}
}
// if (inInferredProjectOnly) {
// openFileRoots.push(rootFile);
// if (rootFile.containingProjects.some(p => p.projectKind !== ProjectKind.Inferred)) {
// // file was included in non-inferred project - drop old inferred project
// }
// else {
// openFileRoots.push(rootFile);
// }
// let inferredProjectsToRemove: Project[];
// for (const p of rootFile.containingProjects) {
// if (p.projectKind !== ProjectKind.Inferred) {
// // file was included in non-inferred project - drop old inferred project
// }
// }
// const rootedProject = rootFile.defaultProject;
@ -979,29 +1031,19 @@ namespace ts.server {
this.printProjects();
}
getScriptInfo(uncheckedFileName: string) {
return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName));
}
getScriptInfoForNormalizedPath(fileName: NormalizedPath) {
return this.filenameToScriptInfo.get(fileName);
}
/**
* Open file whose contents is managed by the client
* @param filename is absolute pathname
* @param fileContent is a known version of the file content that is more up to date than the one on disk
*/
openClientFile(fileName: string, fileContent?: string, scriptKind?: ScriptKind): { configFileName?: string, configFileErrors?: Diagnostic[] } {
openClientFile(fileName: string, fileContent?: string, scriptKind?: ScriptKind): OpenConfiguredProjectResult {
return this.openClientFileWithNormalizedPath(toNormalizedPath(fileName), fileContent, scriptKind);
}
openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind): { configFileName?: string, configFileErrors?: Diagnostic[] } {
let configFileName: string;
let configFileErrors: Diagnostic[];
if (!this.findContainingExternalProject(fileName)) {
({ configFileName, configFileErrors } = this.openOrUpdateConfiguredProjectForFile(fileName));
}
openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind): OpenConfiguredProjectResult {
const { configFileName = undefined, configFileErrors = undefined }: OpenConfiguredProjectResult = this.findContainingExternalProject(fileName)
? {}
: this.openOrUpdateConfiguredProjectForFile(fileName);
// at this point if file is the part of some configured/external project then this project should be created
const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind);
@ -1015,7 +1057,7 @@ namespace ts.server {
* @param filename is absolute pathname
*/
closeClientFile(uncheckedFileName: string) {
const info = this.filenameToScriptInfo.get(toNormalizedPath(uncheckedFileName));
const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName));
if (info) {
this.closeOpenFile(info);
info.isOpen = false;
@ -1024,22 +1066,22 @@ namespace ts.server {
}
getDefaultProjectForFile(fileName: NormalizedPath) {
const scriptInfo = this.filenameToScriptInfo.get(fileName);
const scriptInfo = this.getScriptInfoForNormalizedPath(fileName);
return scriptInfo && scriptInfo.getDefaultProject();
}
private syncExternalFilesList(knownProjects: protocol.ProjectVersionInfo[], projects: Project[], result: protocol.ProjectFiles[]): void {
for (const proj of projects) {
const knownProject = ts.forEach(knownProjects, p => p.projectName === proj.getProjectName() && p);
private collectChanges(lastKnownProjectVersions: protocol.ProjectVersionInfo[], currentProjects: Project[], result: protocol.ProjectFiles[]): void {
for (const proj of currentProjects) {
const knownProject = forEach(lastKnownProjectVersions, p => p.projectName === proj.getProjectName() && p);
result.push(proj.getChangesSinceVersion(knownProject && knownProject.version));
}
}
synchronizeProjectList(knownProjects: protocol.ProjectVersionInfo[]): protocol.ProjectFiles[] {
const files: protocol.ProjectFiles[] = [];
this.syncExternalFilesList(knownProjects, this.externalProjects, files);
this.syncExternalFilesList(knownProjects, this.configuredProjects, files);
this.syncExternalFilesList(knownProjects, this.inferredProjects, files);
this.collectChanges(knownProjects, this.externalProjects, files);
this.collectChanges(knownProjects, this.configuredProjects, files);
this.collectChanges(knownProjects, this.inferredProjects, files);
return files;
}
@ -1053,6 +1095,7 @@ namespace ts.server {
for (const file of changedFiles) {
const scriptInfo = this.getScriptInfo(file.fileName);
Debug.assert(!!scriptInfo);
// apply changes in reverse order
for (let i = file.changes.length - 1; i >= 0; i--) {
const change = file.changes[i];
scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText);
@ -1074,9 +1117,9 @@ namespace ts.server {
const configuredProject = this.findConfiguredProjectByProjectName(configFile);
if (configuredProject) {
this.removeProject(configuredProject);
this.updateProjectStructure();
}
}
this.updateProjectStructure();
}
else {
// close external project
@ -1092,33 +1135,33 @@ namespace ts.server {
const externalProject = this.findExternalProjectByProjectName(proj.projectFileName);
if (externalProject) {
this.updateNonInferredProject(externalProject, proj.rootFiles, proj.options);
return;
}
else {
let tsConfigFiles: NormalizedPath[];
const rootFiles: string[] = [];
for (const file of proj.rootFiles) {
if (getBaseFileName(file) === "tsconfig.json") {
(tsConfigFiles || (tsConfigFiles = [])).push(toNormalizedPath(file));
}
else {
rootFiles.push(file);
}
}
if (tsConfigFiles) {
// store the list of tsconfig files that belong to the external project
this.externalProjectToConfiguredProjectMap[proj.projectFileName] = tsConfigFiles;
for (const tsconfigFile of tsConfigFiles) {
const { success, project, errors } = this.openConfigFile(tsconfigFile);
if (success) {
// keep project alive - its lifetime is bound to the lifetime of containing external project
project.addOpenRef();
}
}
let tsConfigFiles: NormalizedPath[];
const rootFiles: string[] = [];
for (const file of proj.rootFiles) {
if (getBaseFileName(file) === "tsconfig.json") {
(tsConfigFiles || (tsConfigFiles = [])).push(toNormalizedPath(file));
}
else {
this.createAndAddExternalProject(proj.projectFileName, proj.rootFiles, proj.options);
rootFiles.push(file);
}
}
if (tsConfigFiles) {
// store the list of tsconfig files that belong to the external project
this.externalProjectToConfiguredProjectMap[proj.projectFileName] = tsConfigFiles;
for (const tsconfigFile of tsConfigFiles) {
const { success, project, errors } = this.openConfigFile(tsconfigFile);
if (success) {
// keep project alive - its lifetime is bound to the lifetime of containing external project
project.addOpenRef();
}
}
}
else {
this.createAndAddExternalProject(proj.projectFileName, proj.rootFiles, proj.options);
}
}
}
}

View File

@ -72,6 +72,10 @@ namespace ts.server {
return this.project.getProjectVersion();
}
getCompilationSettings() {
return this.compilationSettings;
}
getCancellationToken() {
return this.cancellationToken;
}
@ -90,53 +94,30 @@ namespace ts.server {
}
getScriptSnapshot(filename: string): ts.IScriptSnapshot {
const scriptInfo = this.project.getScriptInfo(filename);
const scriptInfo = this.project.getScriptInfoLSHost(filename);
if (scriptInfo) {
return scriptInfo.snap();
}
}
setCompilationSettings(opt: ts.CompilerOptions) {
this.compilationSettings = opt;
// conservatively assume that changing compiler options might affect module resolution strategy
this.resolvedModuleNames.clear();
this.resolvedTypeReferenceDirectives.clear();
}
getCompilationSettings() {
// change this to return active project settings for file
return this.compilationSettings;
}
getScriptFileNames() {
return this.project.getRootFiles();
}
getScriptKind(fileName: string) {
const info = this.project.getScriptInfo(fileName);
const info = this.project.getScriptInfoLSHost(fileName);
return info && info.scriptKind;
}
getScriptVersion(filename: string) {
return this.project.getScriptInfo(filename).getLatestVersion();
const info = this.project.getScriptInfoLSHost(filename);
return info && info.getLatestVersion();
}
getCurrentDirectory(): string {
return "";
}
removeReferencedFile(info: ScriptInfo) {
if (!info.isOpen) {
this.resolvedModuleNames.remove(info.path);
this.resolvedTypeReferenceDirectives.remove(info.path);
}
}
removeRoot(info: ScriptInfo) {
this.resolvedModuleNames.remove(info.path);
this.resolvedTypeReferenceDirectives.remove(info.path);
}
resolvePath(path: string): string {
return this.host.resolvePath(path);
}
@ -156,5 +137,17 @@ namespace ts.server {
getDirectories(path: string): string[] {
return this.host.getDirectories(path);
}
notifyFileRemoved(info: ScriptInfo) {
this.resolvedModuleNames.remove(info.path);
this.resolvedTypeReferenceDirectives.remove(info.path);
}
setCompilationSettings(opt: ts.CompilerOptions) {
this.compilationSettings = opt;
// conservatively assume that changing compiler options might affect module resolution strategy
this.resolvedModuleNames.clear();
this.resolvedTypeReferenceDirectives.clear();
}
}
}

View File

@ -4,14 +4,22 @@
/// <reference path="lshost.ts"/>
namespace ts.server {
export enum ProjectKind {
Inferred,
Configured,
External
}
function remove<T>(items: T[], item: T) {
const index = items.indexOf(item);
if (index >= 0) {
items.splice(index, 1);
}
}
export abstract class Project {
private rootFiles: ScriptInfo[] = [];
private readonly rootFiles: ScriptInfo[] = [];
private readonly rootFilesMap: FileMap<ScriptInfo> = createFileMap<ScriptInfo>();
private lsHost: ServerLanguageServiceHost;
private program: ts.Program;
@ -130,10 +138,8 @@ namespace ts.server {
containsFile(filename: NormalizedPath, requireOpen?: boolean) {
const info = this.projectService.getScriptInfoForNormalizedPath(filename);
if (info) {
if ((!requireOpen) || info.isOpen) {
return this.containsScriptInfo(info);
}
if (info && (info.isOpen || !requireOpen)) {
return this.containsScriptInfo(info);
}
}
@ -153,12 +159,13 @@ namespace ts.server {
}
removeFile(info: ScriptInfo, detachFromProject: boolean = true) {
if (!this.removeRoot(info)) {
this.removeReferencedFile(info)
}
this.removeRootFileIfNecessary(info);
this.lsHost.notifyFileRemoved(info);
if (detachFromProject) {
info.detachFromProject(this);
}
this.markAsDirty();
}
@ -179,14 +186,31 @@ namespace ts.server {
// - newProgram is different from the old program and structure of the old program was not reused.
if (!oldProgram || (this.program !== oldProgram && !oldProgram.structureIsReused)) {
this.projectStructureVersion++;
if (oldProgram) {
for (const f of oldProgram.getSourceFiles()) {
if (!this.program.getSourceFileByPath(f.path)) {
// new program does not contain this file - detach it from the project
const scriptInfoToDetach = this.projectService.getScriptInfo(f.fileName);
if (scriptInfoToDetach) {
scriptInfoToDetach.detachFromProject(this);
}
}
}
}
}
}
getScriptInfoLSHost(fileName: string) {
const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false);
if (scriptInfo) {
scriptInfo.attachToProject(this);
}
return scriptInfo;
}
getScriptInfoForNormalizedPath(fileName: NormalizedPath) {
const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false);
if (scriptInfo && scriptInfo.attachToProject(this)) {
this.markAsDirty();
}
Debug.assert(!scriptInfo || scriptInfo.isAttached(this));
return scriptInfo;
}
@ -215,19 +239,23 @@ namespace ts.server {
}
}
saveTo(filename: string, tmpfilename: string) {
const script = this.getScriptInfo(filename);
saveTo(filename: NormalizedPath, tmpfilename: NormalizedPath) {
const script = this.projectService.getScriptInfoForNormalizedPath(filename);
if (script) {
Debug.assert(script.isAttached(this));
const snap = script.snap();
this.projectService.host.writeFile(tmpfilename, snap.getText(0, snap.getLength()));
}
}
reloadScript(filename: NormalizedPath, cb: () => void) {
const script = this.getScriptInfoForNormalizedPath(filename);
reloadScript(filename: NormalizedPath): boolean {
const script = this.projectService.getScriptInfoForNormalizedPath(filename);
if (script) {
script.reloadFromFile(filename, cb);
Debug.assert(script.isAttached(this));
script.reloadFromFile();
return true;
}
return false;
}
getChangesSinceVersion(lastKnownVersion?: number): protocol.ProjectFiles {
@ -274,18 +302,11 @@ namespace ts.server {
}
// remove a root file from project
private removeRoot(info: ScriptInfo): boolean {
private removeRootFileIfNecessary(info: ScriptInfo): void {
if (this.isRoot(info)) {
this.rootFiles = copyListRemovingItem(info, this.rootFiles);
remove(this.rootFiles, info);
this.rootFilesMap.remove(info.path);
this.lsHost.removeRoot(info);
return true;
}
return false;
}
private removeReferencedFile(info: ScriptInfo) {
this.lsHost.removeReferencedFile(info)
}
}

View File

@ -3,15 +3,15 @@
namespace ts.server {
export class ScriptInfo {
private svc: ScriptVersionCache;
/**
* All projects that include this file
*/
readonly containingProjects: Project[] = [];
readonly formatCodeSettings: ts.FormatCodeSettings;
readonly path: Path;
private fileWatcher: FileWatcher;
formatCodeSettings: ts.FormatCodeSettings;
readonly path: Path;
private svc: ScriptVersionCache;
constructor(
private readonly host: ServerHost,
@ -29,11 +29,15 @@ namespace ts.server {
}
attachToProject(project: Project): boolean {
if (!contains(this.containingProjects, project)) {
const isNew = !this.isAttached(project);
if (isNew) {
this.containingProjects.push(project);
return true;
}
return false;
return isNew;
}
isAttached(project: Project) {
return contains(this.containingProjects, project);
}
detachFromProject(project: Project) {
@ -80,8 +84,8 @@ namespace ts.server {
this.markContainingProjectsAsDirty();
}
reloadFromFile(fileName: string, cb?: () => void) {
this.svc.reloadFromFile(fileName, cb)
reloadFromFile() {
this.svc.reloadFromFile(this.fileName);
this.markContainingProjectsAsDirty();
}

View File

@ -297,7 +297,7 @@ namespace ts.server {
return this.currentVersion;
}
reloadFromFile(filename: string, cb?: () => void) {
reloadFromFile(filename: string) {
let content = this.host.readFile(filename);
// If the file doesn't exist or cannot be read, we should
// wipe out its cached content on the server to avoid side effects.
@ -305,8 +305,6 @@ namespace ts.server {
content = "";
}
this.reload(content);
if (cb)
cb();
}
// reload whole script, leaving no change history behind reload

View File

@ -1018,7 +1018,7 @@ namespace ts.server {
scriptInfo.editContent(start, end, args.insertString);
this.changeSeq++;
}
this.updateProjectStructure(this.changeSeq, (n) => n === this.changeSeq);
this.updateProjectStructure(this.changeSeq, n => n === this.changeSeq);
}
}
@ -1028,15 +1028,15 @@ namespace ts.server {
if (project) {
this.changeSeq++;
// make sure no changes happen before this one is finished
project.reloadScript(file, () => {
if (project.reloadScript(file)) {
this.output(undefined, CommandNames.Reload, reqSeq);
});
}
}
}
private saveToTmp(fileName: string, tempFileName: string) {
const file = toNormalizedPath(fileName);
const tmpfile = ts.normalizePath(tempFileName);
const tmpfile = toNormalizedPath(tempFileName);
const project = this.projectService.getDefaultProjectForFile(file);
if (project) {
@ -1267,6 +1267,7 @@ namespace ts.server {
},
[CommandNames.ApplyChangedToOpenFiles]: (request: protocol.ApplyChangedToOpenFilesRequest) => {
this.projectService.applyChangesInOpenFiles(request.arguments.openFiles, request.arguments.changedFiles, request.arguments.closedFiles);
this.changeSeq++;
// TODO: report errors
return this.requiredResponse(true);
},

View File

@ -140,17 +140,13 @@ namespace ts.server {
};
export interface ServerLanguageServiceHost {
getCompilationSettings(): CompilerOptions;
setCompilationSettings(options: CompilerOptions): void;
removeRoot(info: ScriptInfo): void;
removeReferencedFile(info: ScriptInfo): void;
notifyFileRemoved(info: ScriptInfo): void;
}
export const nullLanguageServiceHost: ServerLanguageServiceHost = {
getCompilationSettings: () => undefined,
setCompilationSettings: () => undefined,
removeRoot: () => undefined,
removeReferencedFile: () => undefined
notifyFileRemoved: () => undefined
};
export interface ProjectOptions {