<commit_before>/**
 * Tracks tsb.json and the active project within that
 * Also pushes errors out to clients + keeps the language service + fileModelCache in sync
 */

import utils = require("../../common/utils");
import * as json from "../../common/json";
import * as fsu from "../utils/fsu";
import * as flm from "../workers/fileListing/fileListingMaster";
import * as wd from "../disk/workingDir";
import * as fmc from "../disk/fileModelCache";
import * as tsconfig from "./core/tsconfig";
import * as project from "./core/project";
import * as types from "../../common/types";
import {setErrorsByFilePaths, clearErrors, clearErrorsForFilePath} from "./errorsCache";
import {diagnosticToCodeError} from "./building";
import {makeBlandError} from "../../common/utils";
import {TypedEvent} from "../../common/events";
import equal = require('deep-equal');
import * as session from "../disk/session";
import * as workingDir from "../disk/workingDir";


/** The active project name */
// TODO:
// replace this with ActiveProjectConfigDetails
// Wire it up all the way to sending it to the client
// Then in the client show the longer name (node_modules if present)
// And on clicking it open the file from disk
let activeProjectConfigDetails: ActiveProjectConfigDetails = null;
export let activeProjectConfigDetailsUpdated = new TypedEvent<ActiveProjectConfigDetails>();

/** The name used if we don't find a project */
let implicitProjectName = "__auto__";

/** The currently active project */
let currentProject: project.Project = null;

/** All the available projects */
export let availableProjects = new TypedEvent<ActiveProjectConfigDetails[]>();
function refreshAvailableProjects() {
    return flm.filePathsUpdated.current().then((list) => {
        // Detect some tsconfig.json
        let tsconfigs = list.filePaths.filter(t=> t.endsWith('tsconfig.json'));
        // sort by shortest length first (with extra big weight for node_modules):
        let weightConfig = (config: string) => config.includes('node_modules') ? config.length + 100 : config.length;
        tsconfigs = tsconfigs.sort(function(a, b) {
            return weightConfig(a) - weightConfig(b);
        });

        let projectConfigs: ActiveProjectConfigDetails[] = tsconfigs.map(Utils.tsconfigToActiveProjectConfigDetails);

        // If no tsconfigs add an implicit one!
        if (projectConfigs.length == 0) {
            projectConfigs.push({
                name: implicitProjectName,
                isImplicit: true,
            });
        };

        availableProjects.emit(projectConfigs);
    });
}

/** on server start */
export function start() {

    // Keep session on disk in sync
    activeProjectConfigDetailsUpdated.on((ap)=>{
        if (ap.tsconfigFilePath) {
            session.setTsconfigPath(ap.tsconfigFilePath);
        }
    });

    // Resume session
    let ses = session.getDefaultOrNewSession()
    if (ses.relativePathToTsconfig) {
        let tsconfig = workingDir.makeAbsolute(ses.relativePathToTsconfig);
        if (fs.existsSync(tsconfig)) {
            activeProjectConfigDetails = Utils.tsconfigToActiveProjectConfigDetails(tsconfig);
            activeProjectConfigDetailsUpdated.emit(activeProjectConfigDetails);
        }
    }

    refreshAvailableProjects()
        .then(() => sync());
}


/**
  * 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(_activeProjectConfigDetails: ActiveProjectConfigDetails) {
    activeProjectConfigDetails = _activeProjectConfigDetails;
    activeProjectConfigDetailsUpdated.emit(activeProjectConfigDetails);
    clearErrors();
    sync();
}

/** convert project name to current project */
export function sync() {
    availableProjects.current().then((projectConfigs) => {
        let activeProjectName = (activeProjectConfigDetails && activeProjectConfigDetails.name);
        // we are guaranteed as least one project config (which just might be the implicit one)
        let projectConfig = projectConfigs.filter(x=>x.name == activeProjectName)[0] || projectConfigs[0];

        currentProject = null;
        let configFileDetails = ConfigFile.getConfigFileFromDiskOrInMemory(projectConfig)
        currentProject = ConfigFile.createProjectFromConfigFile(configFileDetails);

        // If we made it up to here ... means the config file was good :)
        if (!projectConfig.isImplicit) {
            clearErrorsForFilePath(projectConfig.tsconfigFilePath);
        }

        // Set the active project (the project we get returned might not be the active project name)
        // e.g. on initial load
        if (activeProjectName !== projectConfig.name) {
            activeProjectConfigDetails = projectConfig;
            activeProjectConfigDetailsUpdated.emit(activeProjectConfigDetails);
        }

        refreshAllProjectDiagnostics();
    });
}

/**
 * As soon as we get a new file listing refresh available projects
 */
flm.filePathsUpdated.on(function(data) {
    refreshAvailableProjects();
});

/**
 * As soon as file changes on disk do a full reconcile ....
 */
fmc.savedFileChangedOnDisk.on((evt) => {
    // Check if its a part of the current project .... if not ignore :)
    let proj = GetProject.ifCurrent(evt.filePath)
    if (proj) {
        proj.languageServiceHost.updateScript(evt.filePath, evt.contents);
        refreshAllProjectDiagnostics();
    }
});
/**
 * As soon as edit happens apply to current project
 */
fmc.didEdit.on((evt) => {
    let proj = GetProject.ifCurrent(evt.filePath)
    if (proj) {
        proj.languageServiceHost.editScript(evt.filePath, evt.edit.from, evt.edit.to, evt.edit.newText);
        // For debugging
        // console.log(proj.languageService.getSourceFile(evt.filePath).text);

        // update errors for this file
        let diagnostics = proj.getDiagnosticsForFile(evt.filePath);
        let errors = diagnostics.map(diagnosticToCodeError);
        setErrorsByFilePaths([evt.filePath], errors);

        // After a while update all project diagnostics as well
        refreshAllProjectDiagnostics();
    }

    // Also watch edits to the current config file
    let currentConfigFilePath = activeProjectConfigDetails && activeProjectConfigDetails.tsconfigFilePath;
    if (evt.filePath == currentConfigFilePath){
        sync();
    }
});

/**
 * If there hasn't been a request for a while then we refresh
 * As its a bit slow to get *all* the errors
 */
var refreshAllProjectDiagnostics = utils.debounce(() => {
    if (currentProject) {
        // Send all the errors from the project files:
        let diagnostics = currentProject.getDiagnostics();
        let errors = diagnostics.map(diagnosticToCodeError);
        let filePaths = currentProject.getProjectSourceFiles().map(x=> x.fileName);
        setErrorsByFilePaths(filePaths, errors);
    }
}, 2000);


/**
 * Utility functions to convert a `configFile` to a `project`
 */
import fs = require("fs");
import path = require("path");
import {Project, languageServiceHost} from "./core/project";
import {getOpenFiles} from "../disk/fileModelCache";
namespace ConfigFile {

    /** Create a project from a project file */
    export function createProjectFromConfigFile(configFile: tsconfig.TypeScriptConfigFileDetails) {
        var project = new Project(configFile);

        // Update the language service host for any unsaved changes
        getOpenFiles().forEach(fileModel=> {
            if (project.includesSourceFile(fileModel.config.filePath)) {
                project.languageServiceHost.updateScript(fileModel.config.filePath, fileModel.getContents());
            }
        });

        return project;
    }

    /**
     * Project file error reporting
     */
    function reportProjectFileErrors(ex: Error, filePath: string) {
        var err: Error = ex;<commit_msg>debounce file errors as well<commit_after>/**
 * Tracks tsb.json and the active project within that
 * Also pushes errors out to clients + keeps the language service + fileModelCache in sync
 */

import utils = require("../../common/utils");
import * as json from "../../common/json";
import * as fsu from "../utils/fsu";
import * as flm from "../workers/fileListing/fileListingMaster";
import * as wd from "../disk/workingDir";
import * as fmc from "../disk/fileModelCache";
import * as tsconfig from "./core/tsconfig";
import * as project from "./core/project";
import * as types from "../../common/types";
import {setErrorsByFilePaths, clearErrors, clearErrorsForFilePath} from "./errorsCache";
import {diagnosticToCodeError} from "./building";
import {makeBlandError} from "../../common/utils";
import {TypedEvent} from "../../common/events";
import equal = require('deep-equal');
import * as session from "../disk/session";
import * as workingDir from "../disk/workingDir";


/** The active project name */
// TODO:
// replace this with ActiveProjectConfigDetails
// Wire it up all the way to sending it to the client
// Then in the client show the longer name (node_modules if present)
// And on clicking it open the file from disk
let activeProjectConfigDetails: ActiveProjectConfigDetails = null;
export let activeProjectConfigDetailsUpdated = new TypedEvent<ActiveProjectConfigDetails>();

/** The name used if we don't find a project */
let implicitProjectName = "__auto__";

/** The currently active project */
let currentProject: project.Project = null;

/** All the available projects */
export let availableProjects = new TypedEvent<ActiveProjectConfigDetails[]>();
function refreshAvailableProjects() {
    return flm.filePathsUpdated.current().then((list) => {
        // Detect some tsconfig.json
        let tsconfigs = list.filePaths.filter(t=> t.endsWith('tsconfig.json'));
        // sort by shortest length first (with extra big weight for node_modules):
        let weightConfig = (config: string) => config.includes('node_modules') ? config.length + 100 : config.length;
        tsconfigs = tsconfigs.sort(function(a, b) {
            return weightConfig(a) - weightConfig(b);
        });

        let projectConfigs: ActiveProjectConfigDetails[] = tsconfigs.map(Utils.tsconfigToActiveProjectConfigDetails);

        // If no tsconfigs add an implicit one!
        if (projectConfigs.length == 0) {
            projectConfigs.push({
                name: implicitProjectName,
                isImplicit: true,
            });
        };

        availableProjects.emit(projectConfigs);
    });
}

/** on server start */
export function start() {

    // Keep session on disk in sync
    activeProjectConfigDetailsUpdated.on((ap)=>{
        if (ap.tsconfigFilePath) {
            session.setTsconfigPath(ap.tsconfigFilePath);
        }
    });

    // Resume session
    let ses = session.getDefaultOrNewSession()
    if (ses.relativePathToTsconfig) {
        let tsconfig = workingDir.makeAbsolute(ses.relativePathToTsconfig);
        if (fs.existsSync(tsconfig)) {
            activeProjectConfigDetails = Utils.tsconfigToActiveProjectConfigDetails(tsconfig);
            activeProjectConfigDetailsUpdated.emit(activeProjectConfigDetails);
        }
    }

    refreshAvailableProjects()
        .then(() => sync());
}


/**
  * 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(_activeProjectConfigDetails: ActiveProjectConfigDetails) {
    activeProjectConfigDetails = _activeProjectConfigDetails;
    activeProjectConfigDetailsUpdated.emit(activeProjectConfigDetails);
    clearErrors();
    sync();
}

/** convert project name to current project */
export function sync() {
    availableProjects.current().then((projectConfigs) => {
        let activeProjectName = (activeProjectConfigDetails && activeProjectConfigDetails.name);
        // we are guaranteed as least one project config (which just might be the implicit one)
        let projectConfig = projectConfigs.filter(x=>x.name == activeProjectName)[0] || projectConfigs[0];

        currentProject = null;
        let configFileDetails = ConfigFile.getConfigFileFromDiskOrInMemory(projectConfig)
        currentProject = ConfigFile.createProjectFromConfigFile(configFileDetails);

        // If we made it up to here ... means the config file was good :)
        if (!projectConfig.isImplicit) {
            clearErrorsForFilePath(projectConfig.tsconfigFilePath);
        }

        // Set the active project (the project we get returned might not be the active project name)
        // e.g. on initial load
        if (activeProjectName !== projectConfig.name) {
            activeProjectConfigDetails = projectConfig;
            activeProjectConfigDetailsUpdated.emit(activeProjectConfigDetails);
        }

        refreshAllProjectDiagnostics();
    });
}

/**
 * As soon as we get a new file listing refresh available projects
 */
flm.filePathsUpdated.on(function(data) {
    refreshAvailableProjects();
});

/**
 * As soon as file changes on disk do a full reconcile ....
 */
fmc.savedFileChangedOnDisk.on((evt) => {
    // Check if its a part of the current project .... if not ignore :)
    let proj = GetProject.ifCurrent(evt.filePath)
    if (proj) {
        proj.languageServiceHost.updateScript(evt.filePath, evt.contents);
        refreshAllProjectDiagnostics();
    }
});
/**
 * As soon as edit happens apply to current project
 */
fmc.didEdit.on((evt) => {
    let proj = GetProject.ifCurrent(evt.filePath)
    if (proj) {
        proj.languageServiceHost.editScript(evt.filePath, evt.edit.from, evt.edit.to, evt.edit.newText);
        // For debugging
        // console.log(proj.languageService.getSourceFile(evt.filePath).text);

        // update errors for this file
        refreshFileDiagnostics(evt.filePath);

        // After a while update all project diagnostics as well
        refreshAllProjectDiagnostics();
    }

    // Also watch edits to the current config file
    let currentConfigFilePath = activeProjectConfigDetails && activeProjectConfigDetails.tsconfigFilePath;
    if (evt.filePath == currentConfigFilePath){
        sync();
    }
});

/**
 * If there hasn't been a request for a while then we refresh
 * As its a bit slow to get *all* the errors
 */
var refreshAllProjectDiagnostics = utils.debounce(() => {
    if (currentProject) {
        // Send all the errors from the project files:
        let diagnostics = currentProject.getDiagnostics();
        let errors = diagnostics.map(diagnosticToCodeError);
        let filePaths = currentProject.getProjectSourceFiles().map(x=> x.fileName);
        setErrorsByFilePaths(filePaths, errors);
    }
}, 2000);

/**
 * Constantly streaming this is slow for large files so this is debounced as well
 */
var 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 fs = require("fs");
import path = require("path");
import {Project, languageServiceHost} from "./core/project";
import {getOpenFiles} from "../disk/fileModelCache";
namespace ConfigFile {

    /** Create a project from a project file */
    export function createProjectFromConfigFile(configFile: tsconfig.TypeScriptConfigFileDetails) {
        var project = new Project(configFile);

        // Update the language service host for any unsaved changes
        getOpenFiles().forEach(fileModel=> {
            if (project.includesSourceFile(fileModel.config.filePath)) {
                project.languageServiceHost.updateScript(fileModel.config.filePath, fileModel.getContents());
            }
        });

        return project;
    }

    /**
     * Project file error reporting
     */
    function reportProjectFileErrors(ex: Error, filePath: string) {
        var err: Error = ex;
{'fraction_non_alphanumeric': 0.10679431747992588, 'fraction_numerical': 0.0014823965410747375, 'mean_word_length': 3.2585481325618098, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 32, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7567460', 'n_tokens_mistral': 4241, 'n_tokens_neox': 4183, 'n_words': 1249}