diff --git a/extensions/search-rg/src/extension.ts b/extensions/search-rg/src/extension.ts index 60c2e40815060ff9842e17e6862c518b2aa36088..8b4e9963e6e0a8634b1ac3a4ee4d1e2fadab0866 100644 --- a/extensions/search-rg/src/extension.ts +++ b/extensions/search-rg/src/extension.ts @@ -17,42 +17,47 @@ export function activate(): void { } } -type SearchEngine = RipgrepFileSearchEngine | RipgrepTextSearchEngine; - class RipgrepSearchProvider implements vscode.FileIndexProvider, vscode.TextSearchProvider { - private inProgress: Set = new Set(); + private inProgress: Set = new Set(); constructor(private outputChannel: vscode.OutputChannel) { process.once('exit', () => this.dispose()); } - provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Promise { const engine = new RipgrepTextSearchEngine(this.outputChannel); - return this.withEngine(engine, () => engine.provideTextSearchResults(query, options, progress, token)); + return this.withToken(token, token => engine.provideTextSearchResults(query, options, progress, token)); } provideFileIndex(options: vscode.FileSearchOptions, token: vscode.CancellationToken): Thenable { const engine = new RipgrepFileSearchEngine(this.outputChannel); const results: vscode.Uri[] = []; - const onResult = relativePathMatch => { + const onResult = (relativePathMatch: string) => { results.push(vscode.Uri.file(options.folder.fsPath + '/' + relativePathMatch)); }; - return this.withEngine(engine, () => engine.provideFileSearchResults(options, { report: onResult }, token)) + return this.withToken(token, token => engine.provideFileSearchResults(options, { report: onResult }, token)) .then(() => results); } - private withEngine(engine: SearchEngine, fn: () => Thenable): Thenable { - this.inProgress.add(engine); - return fn().then(result => { - this.inProgress.delete(engine); + private async withToken(token: vscode.CancellationToken, fn: (token: vscode.CancellationToken) => Thenable): Promise { + const merged = mergedTokenSource(token); + this.inProgress.add(merged); + const result = await fn(merged.token); + this.inProgress.delete(merged); - return result; - }); + return result; } private dispose() { this.inProgress.forEach(engine => engine.cancel()); } -} \ No newline at end of file +} + +function mergedTokenSource(token: vscode.CancellationToken): vscode.CancellationTokenSource { + const tokenSource = new vscode.CancellationTokenSource(); + token.onCancellationRequested(() => tokenSource.cancel()); + + return tokenSource; +} diff --git a/extensions/search-rg/src/ripgrepFileSearch.ts b/extensions/search-rg/src/ripgrepFileSearch.ts index de95a2fe39f2711e1d70f495fe9458beec830a41..fd7aaec876a4ff480ad82ecd21599d0fb50141b8 100644 --- a/extensions/search-rg/src/ripgrepFileSearch.ts +++ b/extensions/search-rg/src/ripgrepFileSearch.ts @@ -10,7 +10,7 @@ import * as vscode from 'vscode'; import { normalizeNFC, normalizeNFD } from './normalization'; import { rgPath } from './ripgrep'; import { rgErrorMsgForDisplay } from './ripgrepTextSearch'; -import { anchorGlob } from './utils'; +import { anchorGlob, Maybe } from './utils'; const isMac = process.platform === 'darwin'; @@ -18,18 +18,8 @@ const isMac = process.platform === 'darwin'; const rgDiskPath = rgPath.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.unpacked'); export class RipgrepFileSearchEngine { - private rgProc: cp.ChildProcess; - private isDone: boolean; - constructor(private outputChannel: vscode.OutputChannel) { } - cancel() { - this.isDone = true; - if (this.rgProc) { - this.rgProc.kill(); - } - } - provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { this.outputChannel.appendLine(`provideFileSearchResults ${JSON.stringify({ ...options, @@ -39,7 +29,16 @@ export class RipgrepFileSearchEngine { })}`); return new Promise((resolve, reject) => { - token.onCancellationRequested(() => this.cancel()); + let isDone = false; + + const cancel = () => { + isDone = true; + if (rgProc) { + rgProc.kill(); + } + }; + + token.onCancellationRequested(() => cancel()); const rgArgs = getRgArgs(options); @@ -50,22 +49,22 @@ export class RipgrepFileSearchEngine { .join(' '); this.outputChannel.appendLine(`rg ${escapedArgs}\n - cwd: ${cwd}\n`); - this.rgProc = cp.spawn(rgDiskPath, rgArgs, { cwd }); + let rgProc: Maybe = cp.spawn(rgDiskPath, rgArgs, { cwd }); - this.rgProc.on('error', e => { + rgProc.on('error', e => { console.log(e); reject(e); }); let leftover = ''; - this.collectStdout(this.rgProc, (err, stdout, last) => { + this.collectStdout(rgProc, (err, stdout, last) => { if (err) { reject(err); return; } // Mac: uses NFD unicode form on disk, but we want NFC - const normalized = leftover + (isMac ? normalizeNFC(stdout) : stdout); + const normalized = leftover + (isMac ? normalizeNFC(stdout || '') : stdout); const relativeFiles = normalized.split('\n'); if (last) { @@ -75,7 +74,7 @@ export class RipgrepFileSearchEngine { relativeFiles.pop(); } } else { - leftover = relativeFiles.pop(); + leftover = relativeFiles.pop(); } if (relativeFiles.length && relativeFiles[0].indexOf('\n') !== -1) { @@ -88,11 +87,11 @@ export class RipgrepFileSearchEngine { }); if (last) { - if (this.isDone) { + if (isDone) { resolve(); } else { // Trigger last result - this.rgProc = null; + rgProc = null; if (err) { reject(err); } else { @@ -104,8 +103,8 @@ export class RipgrepFileSearchEngine { }); } - private collectStdout(cmd: cp.ChildProcess, cb: (err: Error, stdout?: string, last?: boolean) => void): void { - let onData = (err: Error, stdout?: string, last?: boolean) => { + private collectStdout(cmd: cp.ChildProcess, cb: (err?: Error, stdout?: string, last?: boolean) => void): void { + let onData = (err?: Error, stdout?: string, last?: boolean) => { if (err || last) { onData = () => { }; } @@ -136,19 +135,19 @@ export class RipgrepFileSearchEngine { cmd.on('close', (code: number) => { // ripgrep returns code=1 when no results are found - let stderrText, displayMsg: string; + let stderrText, displayMsg: Maybe; if (!gotData && (stderrText = this.decodeData(stderr)) && (displayMsg = rgErrorMsgForDisplay(stderrText))) { onData(new Error(`command failed with error code ${code}: ${displayMsg}`)); } else { - onData(null, '', true); + onData(undefined, '', true); } }); } - private forwardData(stream: Readable, cb: (err: Error, stdout?: string) => void): NodeStringDecoder { + private forwardData(stream: Readable, cb: (err?: Error, stdout?: string) => void): NodeStringDecoder { const decoder = new StringDecoder(); stream.on('data', (data: Buffer) => { - cb(null, decoder.write(data)); + cb(undefined, decoder.write(data)); }); return decoder; } diff --git a/extensions/search-rg/src/ripgrepTextSearch.ts b/extensions/search-rg/src/ripgrepTextSearch.ts index fae2fa7ba18bdb1922bfb7390ee57fe0353b175d..2af9d686d3d293543cc5b371fc5a01f0b188c835 100644 --- a/extensions/search-rg/src/ripgrepTextSearch.ts +++ b/extensions/search-rg/src/ripgrepTextSearch.ts @@ -9,31 +9,14 @@ import * as path from 'path'; import { NodeStringDecoder, StringDecoder } from 'string_decoder'; import * as vscode from 'vscode'; import { rgPath } from './ripgrep'; -import { anchorGlob, createTextSearchResult } from './utils'; +import { anchorGlob, createTextSearchResult, Maybe } from './utils'; // If vscode-ripgrep is in an .asar file, then the binary is unpacked. const rgDiskPath = rgPath.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.unpacked'); export class RipgrepTextSearchEngine { - private isDone = false; - private rgProc: cp.ChildProcess; - - private ripgrepParser: RipgrepParser; - constructor(private outputChannel: vscode.OutputChannel) { } - cancel() { - this.isDone = true; - - if (this.rgProc) { - this.rgProc.kill(); - } - - if (this.ripgrepParser) { - this.ripgrepParser.cancel(); - } - } - provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { this.outputChannel.appendLine(`provideTextSearchResults ${query.pattern}, ${JSON.stringify({ ...options, @@ -43,7 +26,7 @@ export class RipgrepTextSearchEngine { })}`); return new Promise((resolve, reject) => { - token.onCancellationRequested(() => this.cancel()); + token.onCancellationRequested(() => cancel()); const rgArgs = getRgArgs(query, options); @@ -54,51 +37,64 @@ export class RipgrepTextSearchEngine { .join(' '); this.outputChannel.appendLine(`rg ${escapedArgs}\n - cwd: ${cwd}`); - this.rgProc = cp.spawn(rgDiskPath, rgArgs, { cwd }); - this.rgProc.on('error', e => { + let rgProc: Maybe = cp.spawn(rgDiskPath, rgArgs, { cwd }); + rgProc.on('error', e => { console.error(e); this.outputChannel.append('Error: ' + (e && e.message)); reject(e); }); let gotResult = false; - this.ripgrepParser = new RipgrepParser(options.maxResults, cwd, options.previewOptions); - this.ripgrepParser.on('result', (match: vscode.TextSearchResult) => { + const ripgrepParser = new RipgrepParser(options.maxResults, cwd, options.previewOptions); + ripgrepParser.on('result', (match: vscode.TextSearchResult) => { gotResult = true; progress.report(match); }); + let isDone = false; + const cancel = () => { + isDone = true; + + if (rgProc) { + rgProc.kill(); + } + + if (ripgrepParser) { + ripgrepParser.cancel(); + } + }; + let limitHit = false; - this.ripgrepParser.on('hitLimit', () => { + ripgrepParser.on('hitLimit', () => { limitHit = true; - this.cancel(); + cancel(); }); - this.rgProc.stdout.on('data', data => { - this.ripgrepParser.handleData(data); + rgProc.stdout.on('data', data => { + ripgrepParser.handleData(data); }); let gotData = false; - this.rgProc.stdout.once('data', () => gotData = true); + rgProc.stdout.once('data', () => gotData = true); let stderr = ''; - this.rgProc.stderr.on('data', data => { + rgProc.stderr.on('data', data => { const message = data.toString(); this.outputChannel.append(message); stderr += message; }); - this.rgProc.on('close', code => { + rgProc.on('close', () => { this.outputChannel.appendLine(gotData ? 'Got data from stdout' : 'No data from stdout'); this.outputChannel.appendLine(gotResult ? 'Got result from parser' : 'No result from parser'); this.outputChannel.appendLine(''); - if (this.isDone) { + if (isDone) { resolve({ limitHit }); } else { // Trigger last result - this.ripgrepParser.flush(); - this.rgProc = null; - let displayMsg: string; + ripgrepParser.flush(); + rgProc = null; + let displayMsg: Maybe; if (stderr && !gotData && (displayMsg = rgErrorMsgForDisplay(stderr))) { reject(new Error(displayMsg)); } else { @@ -115,7 +111,7 @@ export class RipgrepTextSearchEngine { * Ripgrep produces stderr output which is not from a fatal error, and we only want the search to be * "failed" when a fatal error was produced. */ -export function rgErrorMsgForDisplay(msg: string): string | undefined { +export function rgErrorMsgForDisplay(msg: string): Maybe { const firstLine = msg.split('\n')[0].trim(); if (firstLine.startsWith('Error parsing regex')) { @@ -150,9 +146,9 @@ export class RipgrepParser extends EventEmitter { public static readonly MATCH_START_MARKER = '\u001b[0m\u001b[31m'; public static readonly MATCH_END_MARKER = '\u001b[0m'; - private currentFile: string; - private remainder: string; - private isDone: boolean; + private currentFile = ''; + private remainder = ''; + private isDone = false; private stringDecoder: NodeStringDecoder; private numResults = 0; @@ -182,7 +178,7 @@ export class RipgrepParser extends EventEmitter { decodedData; const dataLines: string[] = dataStr.split(/\r\n|\n/); - this.remainder = dataLines[dataLines.length - 1] ? dataLines.pop() : null; + this.remainder = dataLines[dataLines.length - 1] ? dataLines.pop() : ''; for (let l = 0; l < dataLines.length; l++) { const outputLine = dataLines[l].trim(); @@ -190,7 +186,7 @@ export class RipgrepParser extends EventEmitter { break; } - let r: RegExpMatchArray; + let r: Maybe; if (r = outputLine.match(RipgrepParser.RESULT_REGEX)) { const lineNum = parseInt(r[1]) - 1; let matchText = r[2]; @@ -202,7 +198,7 @@ export class RipgrepParser extends EventEmitter { } // Line is a result - add to collected results for the current file path - this.handleMatchLine(outputLine, lineNum, matchText); + this.handleMatchLine(lineNum, matchText); } else if (r = outputLine.match(RipgrepParser.FILE_REGEX)) { this.currentFile = r[1]; } else { @@ -211,7 +207,7 @@ export class RipgrepParser extends EventEmitter { } } - private handleMatchLine(outputLine: string, lineNum: number, lineText: string): void { + private handleMatchLine(lineNum: number, lineText: string): void { if (lineNum === 0) { lineText = stripUTF8BOM(lineText); } @@ -332,9 +328,9 @@ function getRgArgs(query: vscode.TextSearchQuery, options: vscode.TextSearchOpti query.pattern = '\\-\\-'; } - let searchPatternAfterDoubleDashes: string; + let searchPatternAfterDoubleDashes: Maybe; if (query.isWordMatch) { - const regexp = createRegExp(query.pattern, query.isRegExp, { wholeWord: query.isWordMatch }); + const regexp = createRegExp(query.pattern, !!query.isRegExp, { wholeWord: query.isWordMatch }); const regexpStr = regexp.source.replace(/\\\//g, '/'); // RegExp.source arbitrarily returns escaped slashes. Search and destroy. args.push('--regexp', regexpStr); } else if (query.isRegExp) { @@ -407,10 +403,8 @@ function escapeRegExpCharacters(value: string): string { const UTF8_BOM = 65279; -const UTF8_BOM_CHARACTER = String.fromCharCode(UTF8_BOM); - function startsWithUTF8BOM(str: string): boolean { - return (str && str.length > 0 && str.charCodeAt(0) === UTF8_BOM); + return !!(str && str.length > 0 && str.charCodeAt(0) === UTF8_BOM); } function stripUTF8BOM(str: string): string { diff --git a/extensions/search-rg/src/test/searchrg.test.ts b/extensions/search-rg/src/test/searchrg.test.ts index d16d37557f528adda1466099b102572baaa4eef4..dcb6176f0a7001313ef741e83cf617c006026dbb 100644 --- a/extensions/search-rg/src/test/searchrg.test.ts +++ b/extensions/search-rg/src/test/searchrg.test.ts @@ -4,16 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import 'mocha'; -import * as assert from 'assert'; -import * as vscode from 'vscode'; -import * as path from 'path'; -import { createTextSearchResult } from '../utils'; +// TODO./ +// import * as assert from 'assert'; +// import * as vscode from 'vscode'; +// import * as path from 'path'; +// import { createTextSearchResult } from '../utils'; -function createOneLineRange(lineNumber: number, startCol: number, endCol: number): vscode.Range { - return new vscode.Range(lineNumber, startCol, lineNumber, endCol); -} +// function createOneLineRange(lineNumber: number, startCol: number, endCol: number): vscode.Range { +// return new vscode.Range(lineNumber, startCol, lineNumber, endCol); +// } -const uri = vscode.Uri.file('/foo/bar'); +// const uri = vscode.Uri.file('/foo/bar'); suite('search-rg', () => { diff --git a/extensions/search-rg/src/utils.ts b/extensions/search-rg/src/utils.ts index 320bc73118e9fe86a099dbb922e6f860531a94de..1cac75cf016c46d5716419d92ca725886e49a424 100644 --- a/extensions/search-rg/src/utils.ts +++ b/extensions/search-rg/src/utils.ts @@ -6,6 +6,8 @@ import * as path from 'path'; import * as vscode from 'vscode'; +export type Maybe = T | null | undefined; + export function fixDriveC(_path: string): string { const root = path.parse(_path).root; return root.toLowerCase() === 'c:/' ? diff --git a/extensions/search-rg/tsconfig.json b/extensions/search-rg/tsconfig.json index 572f8ef91da8b58962b99dbf02df9ac135c4bd09..489bde02cfa74aed2c62d89691163dbdb914a92d 100644 --- a/extensions/search-rg/tsconfig.json +++ b/extensions/search-rg/tsconfig.json @@ -1,13 +1,8 @@ { + "extends": "../shared.tsconfig.json", "compilerOptions": { - "target": "es5", - "module": "commonjs", - "lib": [ - "es6", - "es2015.promise" - ], "outDir": "./out", - "experimentalDecorators": true + "experimentalDecorators": true, }, "include": [ "src/**/*"