alm
Version:
The best IDE for TypeScript
197 lines (173 loc) • 6.95 kB
text/typescript
/**
* Manages active project.
* Also pushes errors out to clients + keeps the language service in sync
*/
import utils = require("../../../common/utils");
import * as json from "../../../common/json";
import * as tsconfig from "./core/tsconfig";
import * as project from "./core/project";
import * as types from "../../../common/types";
import {errorsCache} from "./cache/tsErrorsCache";
const {setErrorsByFilePaths, clearErrors, clearErrorsForFilePath} = errorsCache;
import {diagnosticToCodeError} from "./modules/building";
import {TypedEvent} from "../../../common/events";
import equal = require('deep-equal');
import * as chalk from "chalk";
import {AvailableProjectConfig,makeBlandError} from "../../../common/types";
import * as fsu from "../../utils/fsu";
import {master as masterType} from "./projectServiceContract";
let master: typeof masterType;
export function setMaster(m: typeof masterType) {
master = m;
}
/** On working */
export const working = new TypedEvent<types.Working>();
/** The active project name */
let activeProjectConfigDetails: types.ProjectDataLoaded = null;
/** The currently active project */
let currentProject: project.Project = null;
/**
* Changes the active project.
* Clear any previously reported errors and recalculate the errors
* This is what the user should call if they want to manually sync as well
*/
export function setActiveProjectConfigDetails(projectData: types.ProjectDataLoaded) {
initialSync = true;
activeProjectConfigDetails = projectData;
currentProject = new project.Project(projectData);
/** Refresh them errors */
clearErrors();
cancelAnyPendingAnalysisAndMarkforRefreshingAllProjectDiagnostics();
/** Return active project for any chaining */
return currentProject;
}
function sync() {
// We need to request new data load from master
master.sync({});
}
/**
* File changing on disk
*/
export function fileEdited(evt: { filePath: string, edits: CodeEdit[] }) {
let proj = GetProject.ifCurrent(evt.filePath)
if (proj) {
evt.edits.forEach(edit=>{
proj.languageServiceHost.applyCodeEdit(evt.filePath, edit.from, edit.to, edit.newText);
// For debugging
// console.log(proj.languageService.getSourceFile(evt.filePath).text);
// update errors for this file if its *heuristically* small
if (edit.from.line < 1000) {
refreshFileDiagnostics(evt.filePath);
}
// After a while update all project diagnostics as well
cancelAnyPendingAnalysisAndMarkforRefreshingAllProjectDiagnostics();
});
}
}
export function fileChangedOnDisk(evt: { filePath: string; contents: string }) {
// Check if its a part of the current project .... if not ignore :)
let proj = GetProject.ifCurrent(evt.filePath)
if (proj) {
proj.languageServiceHost.setContents(evt.filePath, evt.contents);
cancelAnyPendingAnalysisAndMarkforRefreshingAllProjectDiagnostics();
}
}
/**
* If there hasn't been a request for a while then we refresh
* As its a bit slow to get *all* the errors
*/
let initialSync = false;
let cancellationToken: utils.CancellationToken | null = null;
const refreshAllProjectDiagnostics = () => {
if (currentProject) {
const timer = utils.timer();
const projectFilePath = currentProject.configFile.projectFilePath;
if (initialSync) {
console.error(`[TSC] Started Initial Error Analysis: ${projectFilePath}`);
}
else {
console.log(`[TSC] Incremental Error Analysis ${projectFilePath}`);
}
// Get all the errors from the project files:
cancellationToken = utils.cancellationToken();
currentProject.getDiagnostics(cancellationToken).then((diagnostics)=>{
console.error('[TSC] Error Analysis Duration:', timer.seconds);
let errors = diagnostics.map(diagnosticToCodeError);
let filePaths = currentProject.getFilePaths();
setErrorsByFilePaths(filePaths, errors);
console.log(`[TSC] FileCount: ${filePaths.length} `, errors.length? chalk.red(`Errors: ${errors.length}`): chalk.green(`Errors: ${errors.length}`));
initialSync = false;
working.emit({working: false});
})
.catch((res)=>{
console.log('[TSC] Cancelled error analysis');
})
}
};
const cancelAnyPendingAnalysisAndMarkforRefreshingAllProjectDiagnostics = () => {
working.emit({working: true});
if (cancellationToken) {
cancellationToken.cancel();
cancellationToken = null;
}
refreshAllProjectDiagnosticsDebounced();
}
const refreshAllProjectDiagnosticsDebounced = utils.debounce(refreshAllProjectDiagnostics, 3000);
/**
* Constantly streaming this is slow for large files so this is debounced as well
*/
const refreshFileDiagnostics = utils.debounce((filePath:string) => {
let proj = GetProject.ifCurrent(filePath)
if (proj) {
let diagnostics = proj.getDiagnosticsForFile(filePath);
let errors = diagnostics.map(diagnosticToCodeError);
setErrorsByFilePaths([filePath], errors);
}
}, 1000);
/**
* Utility functions to convert a `configFile` to a `project`
*/
import path = require("path");
/** Get project functions */
export namespace GetProject {
/**
* Utility function used all the time
*/
export function ifCurrent(filePath: string): project.Project | undefined {
if (currentProject && currentProject.includesSourceFile(filePath)) {
return currentProject;
}
}
/**
* Sometimes (e.g in the projectService) you want to error out
* because these functions should not be called if there is no active project
*/
export function ifCurrentOrErrorOut(filePath: string): project.Project {
let proj = ifCurrent(filePath);
if (!proj) {
/** This is happening for invalid cases so added some debug assistance */
const isThereAnActiveProject = !!currentProject;
const filePathsCount = isThereAnActiveProject ? currentProject.getProjectSourceFiles().length : 0;
console.log(
{
error: types.errors.CALLED_WHEN_NO_ACTIVE_PROJECT_FOR_FILE_PATH,
filePath,
isThereAnActiveProject,
filePathsCount
}
);
throw new Error(types.errors.CALLED_WHEN_NO_ACTIVE_PROJECT_FOR_FILE_PATH);
}
return proj;
}
/**
* Get current if any OR throw
*/
export function getCurrentIfAny(): project.Project {
if (!currentProject) {
console.error(types.errors.CALLED_WHEN_NO_ACTIVE_PROJECT_GLOBAL);
throw new Error(types.errors.CALLED_WHEN_NO_ACTIVE_PROJECT_GLOBAL);
}
return currentProject;
}
}