alm
Version:
The best IDE for TypeScript
209 lines (174 loc) • 6.2 kB
text/typescript
/**
* Find and replace multi. The single file version is client only.
*/
import * as wd from "../../disk/workingDir";
import * as cp from "child_process";
import {Types} from "../../../socket/socketContract";
import * as utils from "../../../common/utils";
import {TypedEvent} from "../../../common/events";
import * as types from "../../../common/types";
/**
* Maintains current farm state
*/
class FarmState {
constructor(public config: Types.FarmConfig) {
let searchTerm = config.query;
/**
* https://git-scm.com/docs/git-grep
*
* We don't do `--full-name` as that is relative to `.git` parent.
* Without that it is relative to cwd which is better for us.
*
* // General main ones
* n: line number
* I: don't match binary files
*
* // Useful toggles
* w: Match the pattern only at word boundary (also takes into account new lines 💟)
* i: ignore case
*
* E: extended regexp
* F: Don't interpret pattern as regexp
*/
const grep = cp.spawn(`git`, [
`--no-pager`,
`grep`,
`-In`
+ (config.isRegex ? 'E' : 'F')
+ (config.isFullWord ? 'w' : '')
+ (config.isCaseSensitive ? '' : 'i'),
searchTerm,
`--` // signals pathspec
].concat(config.globs));
grep.stdout.on('data', (data) => {
// console.log(`Grep stdout: ${data}`);
/** If no one cares anymore */
if (farmState.disposed) {
grep.kill();
return;
}
/**
* Don't want to run out of memory
*/
if (this.results.length > types.maxCountFindAndReplaceMultiResults) {
grep.kill();
return;
}
// Sample :
// src/typings/express/express.d.ts:907: app.enable('foo')
// src/typings/express/express.d.ts:908: app.disabled('foo')
// String
data = data.toString();
// Split by \n and trim each to get lines
let lines: string[] =
data.split('\n')
.map(x => x.trim())
.filter(x => x);
const newResults: Types.FarmResultDetails[] = lines.map(line => {
let originalLine = line;
// Split line by `:\d:` to get relativeName as first
let relativeFilePath = line.split(/:\d+:/)[0];
line = line.substr(relativeFilePath.length);
// :123: some preview
// =>
// 123: some preview
line = line.substr(1);
// line number!
let lineNumber = line.split(':')[0];
line = line.substr(lineNumber.length);
// : some preview
// =>
// some preview
let preview =
line.split(':').slice(1).join(':')
.trim();
let result: Types.FarmResultDetails = {
filePath: wd.makeAbsolute(relativeFilePath),
line: +lineNumber,
preview: preview
};
/* Debug
console.log(originalLine);
console.log('\n')
console.log(result);
console.log('------------------------------------------\n')
/* */
if (!result.preview) {
// TODO: this is probably because the line boundries are wrong as data comes in.
// We should store the line remainder and prepend to new data.
return null;
}
return result;
}).filter(x=>!!x);
// Add to results
this.results = this.results.concat(newResults);
this.notifyUpdate();
});
grep.stderr.on('data', (data) => {
if (farmState.disposed) return;
console.log(`Grep stderr: ${data}`);
});
grep.on('close', (code) => {
if (farmState.disposed) return;
this.completed = true;
this.notifyUpdate();
if (!code) {
// TODO: Search complete!
}
if (code) {
// Also happens if search returned no results
// console.error(`Grep process exited with code ${code}`);
}
});
}
/**
* Key search state variables
*/
private results: Types.FarmResultDetails[] = [];
private completed = false;
/** Exposed event */
resultsUpdated = new TypedEvent<Types.FarmNotification>();
private notifyUpdate = utils.throttle(() => {
let {results, completed, config} = this;
this.resultsUpdated.emit({ results, completed, config });
}, 500);
/** Allows us to dispose any running search */
private disposed = false;
dispose = () => {
this.disposed = true;
this.completed = true;
this.notifyUpdate();
}
}
/**
* The current farm state
*/
let farmState: FarmState = null;
/**
*
*
* The exposed service API
*
*
*/
/**
* Subscribe to this if you want notifications about any current farming
*/
export const farmResultsUpdated = new TypedEvent<Types.FarmNotification>();
// initiate as completed with no results
farmResultsUpdated.emit({ completed: true, results: [], config: null });
/** Also safely stops any previous running farming */
export function startFarming(cfg: Types.FarmConfig): Promise<{}> {
stopFarmingIfRunning({});
farmState = new FarmState(cfg);
farmResultsUpdated.emit({ completed: false, results: [], config:cfg });
farmState.resultsUpdated.pipe(farmResultsUpdated);
return Promise.resolve({});
}
export function stopFarmingIfRunning(args: {}): Promise<{}> {
if (farmState) {
farmState.dispose();
farmState = null;
}
return Promise.resolve({});
}