alm
Version:
The best IDE for TypeScript
337 lines (293 loc) • 12.1 kB
text/typescript
import * as sw from "../../utils/simpleWorker";
import * as contract from "./fileListingContract";
import * as fs from "fs";
import * as fsu from "../../utils/fsu";
import * as utils from "../../../common/utils";
import * as glob from "glob";
import chokidar = require('chokidar');
import { throttle } from "../../../common/utils";
import path = require('path');
import { TypedEvent } from "../../../common/events";
import * as types from "../../../common/types";
import * as chalk from "chalk";
const maxFileCount = 100000;
/** A Map for faster live calculations */
type LiveList = { [filePath: string]: types.FilePathType };
/** The directory to watch */
let directoryUnderWatch: string;
namespace Worker {
export const echo: typeof contract.worker.echo = (q) => {
return master.increment(q).then((res) => {
return {
text: q.text,
num: res.num
};
});
}
export const setupWatch: typeof contract.worker.setupWatch = (q) => {
directoryUnderWatch = q.directory;
let completed = false;
let liveList: LiveList = {};
// Effectively a list of the mutations that `liveList` is going through after initial sync
let bufferedAdded: types.FilePath[] = [];
let bufferedRemoved: types.FilePath[] = [];
const filterName = (filePath: string) => {
return (
// Remove .git we have no use for that here
!filePath.endsWith('.git') && !filePath.includes('/.git/')
// MAC
&& !filePath.endsWith('.DS_Store')
);
}
// Utility to send new file list
const sendNewFileList = () => {
let filePaths = Object.keys(liveList)
.filter(filterName)
// sort
.sort((a, b) => {
// sub dir wins!
if (b.startsWith(a)) {
return -1;
}
if (a.startsWith(b)) {
return 1;
}
// The next sorts are slow and only done after initial listing!
if (!completed) {
return a.length - b.length;
}
// sort by name
return a.toLowerCase().localeCompare(b.toLowerCase());
})
// Convert ot file path type
.map(filePath => {
let type = liveList[filePath];
return { filePath, type };
});
master.fileListUpdated({
filePaths,
completed
});
// Send out the delta as well
// Unless of course this is the *initial* sending of file listing
if (bufferedAdded.length || bufferedRemoved.length) {
master.fileListingDelta({
addedFilePaths: bufferedAdded.filter(x => filterName(x.filePath)),
removedFilePaths: bufferedRemoved.filter(x => filterName(x.filePath))
});
bufferedAdded = [];
bufferedRemoved = [];
}
};
/**
* Slower version for
* - initial partial serach
* - later updates which might be called a lot because of some directory of files removed
*/
let sendNewFileListThrottled = throttle(sendNewFileList, 1500);
/**
* Utility function to get the listing from a directory
* No side effects in this function
*/
const getListing = (dirPath: string): Promise<types.FilePath[]> => {
return new Promise<types.FilePath[]>((resolve) => {
let mg = new glob.Glob('**', { cwd: dirPath, dot: true }, (e, globResult) => {
if (e) {
console.error('Globbing error:', e);
}
let list = globResult.map(nl => {
let p = fsu.resolve(dirPath, nl);
let type = mg.cache[p] && mg.cache[p] == 'FILE' ? types.FilePathType.File : types.FilePathType.Dir;
return {
filePath: fsu.consistentPath(p),
type,
}
});
resolve(list);
});
});
}
// create initial list using 10x faster glob.Glob!
(function() {
/** These things are coming on a mac for some reason */
const ignoreThisPathThatGlobGivesForUnknownReasons = (filePath: string) => {
return filePath.includes('0.0.0.0') || (filePath.includes('[object Object]'))
}
const cwd = q.directory;
const mg = new glob.Glob('**', { cwd, dot: true }, (e, newList) => {
if (e) {
checkGlobbingError(e);
if (abortGlobbing) {
mg.abort();
// if we don't exit then glob keeps globbing + erroring despite mg.abort()
process.exit();
return;
}
console.error('Globbing error:', e);
}
let list = newList.map(nl => {
let p = fsu.resolve(cwd, nl);
// NOTE: the glob cache also uses consistent path even on windows, hence `fsu.resolve` ^ :)
let type = mg.cache[p] && mg.cache[p] == 'FILE' ? types.FilePathType.File : types.FilePathType.Dir;
if (ignoreThisPathThatGlobGivesForUnknownReasons(nl)) {
// console.log(nl, mg.cache[p]); /// DEBUG
return null;
}
return {
filePath: fsu.consistentPath(p),
type,
}
}).filter(x => !!x);
// Initial search complete!
completed = true;
list.forEach(entry => liveList[entry.filePath] = entry.type);
sendNewFileList();
});
let matchLength = 0
/** Still send the listing while globbing so user gets immediate feedback */
mg.on('match', (match) => {
matchLength++;
if (matchLength > maxFileCount) {
abortDueToTooManyFiles();
return;
}
let p = fsu.resolve(cwd, match);
if (mg.cache[p]) {
if (ignoreThisPathThatGlobGivesForUnknownReasons(match)) {
return;
}
liveList[fsu.consistentPath(p)] = mg.cache[p] == 'FILE' ? types.FilePathType.File : types.FilePathType.Dir;
sendNewFileListThrottled();
}
});
})();
function fileAdded(filePath: string) {
filePath = fsu.consistentPath(filePath);
// Only send if we don't know about this already (because of faster initial scan)
if (!liveList[filePath]) {
let type = types.FilePathType.File;
liveList[filePath] = type;
bufferedAdded.push({
filePath,
type
});
sendNewFileListThrottled();
}
}
function dirAdded(dirPath: string) {
dirPath = fsu.consistentPath(dirPath);
liveList[dirPath] = types.FilePathType.Dir;
bufferedAdded.push({
filePath: dirPath,
type: types.FilePathType.Dir
});
/**
* - glob the folder
* - send the folder throttled
*/
getListing(dirPath).then(res => {
res.forEach(fpDetails => {
if (!liveList[fpDetails.filePath]) {
let type = fpDetails.type
liveList[fpDetails.filePath] = type;
bufferedAdded.push({
filePath: fpDetails.filePath,
type
});
}
});
sendNewFileListThrottled();
}).catch(res => {
console.error('[FLW] DirPath listing failed:', dirPath, res);
});
}
function fileDeleted(filePath: string) {
filePath = fsu.consistentPath(filePath);
delete liveList[filePath];
bufferedRemoved.push({
filePath,
type: types.FilePathType.File
});
sendNewFileListThrottled();
}
function dirDeleted(dirPath: string) {
dirPath = fsu.consistentPath(dirPath);
Object.keys(liveList).forEach(filePath => {
if (filePath.startsWith(dirPath)) {
bufferedRemoved.push({
filePath,
type: liveList[filePath]
});
delete liveList[filePath];
}
});
sendNewFileListThrottled();
}
/** Create watcher */
let watcher = chokidar.watch(directoryUnderWatch, {
/** Don't care about initial as we did that using glob as its faster */
ignoreInitial: true,
// For fixing file permission errors on windows.
// Recommended here : https://github.com/paulmillr/chokidar/issues/446
// Someday.
// Not enabled because the CPU useage goes *way* up.
// /**
// * Use polling, otherwise other files get locked
// * e.g. `npm install foo` will fail sadly on windows
// */
// usePolling: true,
// /**
// * Because we have `usePolling` the external process will most likely work
// * However *we* might not be able to stat a file temporarily when its open
// * e.g. *immediately* after an external `npm install` on windows we get a perm error.
// */
// ignorePermissionErrors: true,
});
// Just the ones that impact file listing
// https://github.com/paulmillr/chokidar#methods--events
watcher.on('add', fileAdded);
watcher.on('addDir', dirAdded);
watcher.on('unlink', fileDeleted);
watcher.on('unlinkDir', dirDeleted);
// Just for changes
watcher.on('change', (filePath) => {
// We have no use for this right now
});
return Promise.resolve({});
}
}
// Ensure that the namespace follows the contract
const _checkTypes: typeof contract.worker = Worker;
// run worker
export const { master } = sw.runWorker({
workerImplementation: Worker,
masterContract: contract.master
});
function debug(...args) {
console.error.apply(console, args);
}
/**
* If its a permission error warn the user that they should start in some project directory
*/
let abortGlobbing = false;
function checkGlobbingError(err: any) {
if (err.path &&
(err.code == 'EPERM' /*win*/ || err.code == 'EACCES' /*mac*/)
) {
abortGlobbing = true;
const errorMessage = `Exiting: Permission error when trying to list ${err.path}.
- Start the IDE in a project folder (e.g. '/your/project')
- Check access to the path`
master.abort({ errorMessage });
}
}
function abortDueToTooManyFiles() {
abortGlobbing = true;
const errorMessage = `Exiting: Too many files in folder. Currently we limit to ${maxFileCount}.
- Start the IDE in a project folder (e.g. '/your/project')`;
master.abort({ errorMessage });
}
process.on('error', () => {
console.log('here');
process.exit();
})