// ***************************************************************************** // Copyright (C) 2017-2018 Ericsson and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { Container } from '@theia/core/shared/inversify'; import { ILogger, isWindows } from '@theia/core'; import { FileUri } from '@theia/core/lib/common/file-uri'; import { MockLogger } from '@theia/core/lib/common/test/mock-logger'; import { RawProcessFactory, RawProcessOptions, RawProcess, ProcessManager } from '@theia/process/lib/node'; import { RipgrepSearchInWorkspaceServer, RgPath } from './ripgrep-search-in-workspace-server'; import { SearchInWorkspaceClient, SearchInWorkspaceResult } from '../common/search-in-workspace-interface'; import * as path from 'path'; import * as temp from 'temp'; import * as fs from 'fs'; import { expect } from 'chai'; import { rgPath as realRgPath } from '@vscode/ripgrep'; // Allow creating temporary files, but remove them when we are done. const track = temp.track(); // The root dirs we'll use to test searching. let rootDirA: string; let rootDirB: string; let rootSubdirA: string; let rootDirAUri: string; let rootDirBUri: string; let rootSubdirAUri: string; // Remember the content of the test files we create, to validate that the // reported line text is right. const fileLines: Map = new Map(); // The class under test. let ripgrepServer: RipgrepSearchInWorkspaceServer; // Mock client that accumulates the returned results in a list. class ResultAccumulator implements SearchInWorkspaceClient { results: SearchInWorkspaceResult[] = []; onDoneCallback: () => void; constructor(onDoneCallback: () => void) { this.onDoneCallback = onDoneCallback; } onResult(searchId: number, result: SearchInWorkspaceResult): void { this.results.push(result); } onDone(searchId: number): void { // Sort the results, so that the order is predictable. this.results.sort(SearchInWorkspaceResult.compare); this.onDoneCallback(); } } // Create a test file relative to rootDir. function createTestFile(filename: string, text: string): void { const dir = getRootPathFromName(filename); fs.writeFileSync(path.join(dir, filename), text); fileLines.set(filename, text.split('\n')); } // Returns the path of the root folder by the file name const getRootPathFromName = (name: string) => { const names: { [file: string]: string } = { carrots: rootDirA, potatoes: rootDirA, pastas: rootDirA, regexes: rootDirA, 'long-line': rootDirA, small: `${rootDirA}/small`, 'file:with:some:colons': rootDirA, 'file with spaces': rootDirA, 'utf8-file': rootDirA, 'special shell characters': rootDirA, 'glob.txt': rootDirA, glob: rootDirA, 'lots-of-matches': rootDirA, orange: rootDirB, folderSubfolder: rootSubdirA, 'orange/navel': `${rootDirA}`, 'orange/hamlin': `${rootDirA}`, 'test/test-spec.ts': `${rootDirA}`, 'small/test/test-spec.ts': `${rootDirA}` }; return names[name]; }; before(() => { rootDirA = track.mkdirSync(); rootDirB = track.mkdirSync(); rootSubdirA = track.mkdirSync({ dir: rootDirA }); rootDirAUri = FileUri.create(rootDirA).toString(); rootDirBUri = FileUri.create(rootDirB).toString(); rootSubdirAUri = FileUri.create(rootSubdirA).toString(); let contents = ''; for (let x = 0; x < 10000; x++) { const str = '' + x; const pad = '00000'; contents += ('long-' + pad.substring(0, pad.length - str.length) + str); } createTestFile('long-line', contents); createTestFile('carrots', `\ This is a carrot. Most carrots are orange, but some carrots are not. Once capitalized, the word carrot looks like this: CARROT. Carrot is a funny word. `); createTestFile('potatoes', `\ Potatoes, unlike carrots, are generally not orange. But sweet potatoes are, it's very confusing. `); createTestFile('pastas', 'pasta pasta'); createTestFile('regexes', `\ aaa hello. x h3lo y hell0h3lllo hello1 `); const smallDirPath = rootDirA + '/small'; fs.mkdirSync(smallDirPath); createTestFile('small', 'A small file.\n'); const copyrightLine = '\ Copyright (C) 2021 and others.'; fs.mkdirSync(smallDirPath + '/test'); createTestFile('small/test/test-spec.ts', copyrightLine); fs.mkdirSync(rootDirA + '/test'); createTestFile('test/test-spec.ts', copyrightLine); fs.mkdirSync(rootDirA + '/orange'); createTestFile('orange/hamlin', '\ Hamlin orange is one of our most cold-hardy sweet oranges. Grown since 1885'); createTestFile('orange/navel', '\ Most well known orange type'); if (!isWindows) { createTestFile('file:with:some:colons', `\ Are you looking for this: --foobar? `); } createTestFile('file with spaces', `\ Are you looking for this: --foobar? `); createTestFile('utf8-file', `\ Var är jag? Varför är jag här? `); createTestFile('special shell characters', `\ If one uses \`salut";\' echo foo && echo bar; "\` as a search term it should not be a problem to find here. `); createTestFile('glob.txt', `\ test -glob patterns `); createTestFile('glob', `\ test --glob patterns `); let lotsOfMatchesText = ''; for (let i = 0; i < 100000; i++) { lotsOfMatchesText += 'lots-of-matches\n'; } createTestFile('lots-of-matches', lotsOfMatchesText); createTestFile('orange', `\ the oranges' orange looks slightly different from carrots' orange. `); createTestFile('folderSubfolder', 'a file in the subfolder of a folder.'); }); // Create an instance of RipgrepSearchInWorkspaceServer which uses rgPath as // the rg binary. function createInstance(rgPath: string): RipgrepSearchInWorkspaceServer { const container = new Container(); container.bind(ILogger).to(MockLogger); container.bind(RipgrepSearchInWorkspaceServer).toSelf(); container.bind(ProcessManager).toSelf().inSingletonScope(); container.bind(RawProcess).toSelf().inTransientScope(); container.bind(RawProcessFactory).toFactory(ctx => (options: RawProcessOptions) => { const child = new Container({ defaultScope: 'Singleton' }); child.parent = ctx.container; child.bind(RawProcessOptions).toConstantValue(options); return child.get(RawProcess); } ); container.bind(RgPath).toConstantValue(rgPath); return container.get(RipgrepSearchInWorkspaceServer); } beforeEach(() => { ripgrepServer = createInstance(realRgPath); }); after(() => { try { track.cleanupSync(); } catch (ex) { console.log("Couldn't cleanup search-in-workspace temp directory.", ex); } }); interface SearchInWorkspaceExpectation { root: string, fileUri: string, line: number, character: number, length: number, lineText: string } // Compare expected and actual search results. // // For convenience, the expected entries do not have their lineText field set // by individual tests. Using on the file and line fields, this function // retrieves the expected line text based on what we have written to the test // files. // // The expected entries should also have the file field set relatively to // rootDir. This function will update the field to contain the absolute path. function compareSearchResults(expected: SearchInWorkspaceExpectation[], actual: SearchInWorkspaceResult[]): void { const allMatches = actual.reduceRight((p, v) => p + v.matches.length, 0); expect(allMatches).eq(expected.length, 'mismatching number of matches across results'); if (actual.length !== expected.length) { return; } for (let i = 0; i < actual.length; i++) { const e = expected[i]; const lines = fileLines.get(e.fileUri); if (lines) { const line = lines[e.line - 1]; e.lineText = line; e.fileUri = FileUri.create(path.join(getRootPathFromName(e.fileUri), e.fileUri)).toString(); const a = actual.find(l => l.fileUri === e.fileUri)!; const match = a.matches.find(m => m.line === e.line && m.character === e.character)!; if (!match) { console.log(a); } expect(match.length).eq(e.length, `match[${i}].length != expected[${i}].length`); if (typeof match.lineText === 'string') { expect(match.lineText).eq(e.lineText, `match[${i}].lineText != expected[${i}].lineText`); } } else { // We don't know this file... expect.fail(); } } } describe('ripgrep-search-in-workspace-server', function (): void { this.timeout(10000); it('should return 1 result when searching for " pasta", respecting the leading whitespace', done => { const pattern = ' pasta'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'pastas', line: 1, character: 6, length: pattern.length, lineText: '' }, ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri]); }); it('should return 1 result when searching for "pasta", respecting the trailing whitespace', done => { const pattern = 'pasta '; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'pastas', line: 1, character: 1, length: pattern.length, lineText: '' }, ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri]); }); // Try some simple patterns with different case. it('should return 7 results when searching for "carrot"', done => { const pattern = 'carrot'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'carrots', line: 1, character: 11, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'carrots', line: 2, character: 6, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'carrots', line: 2, character: 35, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'carrots', line: 3, character: 28, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'carrots', line: 3, character: 52, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'carrots', line: 4, character: 1, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'potatoes', line: 1, character: 18, length: pattern.length, lineText: '' } ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri]); }); it('should return 5 results when searching for "carrot" case sensitive', done => { const pattern = 'carrot'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'carrots', line: 1, character: 11, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'carrots', line: 2, character: 6, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'carrots', line: 2, character: 35, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'carrots', line: 3, character: 28, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'potatoes', line: 1, character: 18, length: pattern.length, lineText: '' } ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri], { matchCase: true }); }); it('should return 4 results when searching for "carrot" matching whole words, case insensitive', done => { const pattern = 'carrot'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'carrots', line: 1, character: 11, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'carrots', line: 3, character: 28, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'carrots', line: 3, character: 52, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'carrots', line: 4, character: 1, length: pattern.length, lineText: '' } ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri], { matchWholeWord: true }); }); it('should return 4 results when searching for "carrot" matching whole words, case sensitive', done => { const pattern = 'carrot'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'carrots', line: 1, character: 11, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'carrots', line: 3, character: 28, length: pattern.length, lineText: '' } ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri], { matchWholeWord: true, matchCase: true }); }); it('should return 1 result when searching for "Carrot"', done => { const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'carrots', line: 4, character: 1, length: 6, lineText: '' }, ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search('Carrot', [rootDirAUri], { matchCase: true }); }); it('should return 0 result when searching for "CarroT"', done => { const pattern = 'CarroT'; const client = new ResultAccumulator(() => { compareSearchResults([], client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri], { matchCase: true }); }); // Try something that we know isn't there. it('should find 0 result when searching for "PINEAPPLE"', done => { const pattern = 'PINEAPPLE'; const client = new ResultAccumulator(() => { compareSearchResults([], client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri]); }); // Try a pattern with a space. it('should find 1 result when searching for "carrots are orange"', done => { const pattern = 'carrots are orange'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'carrots', line: 2, character: 6, length: pattern.length, lineText: '' }, ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri]); }); it('should deal with very long lines."', done => { const pattern = 'long-00400'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'long-line', line: 1, character: 4001, length: pattern.length, lineText: '' }, ]; compareSearchResults(expected, client.results); const match = client.results[0].matches[0]; if (typeof match.lineText === 'string') { done(new Error('unexpected lineText')); } else { expect(match.lineText.text.substring(match.lineText.character - 1, pattern.length + match.lineText.character - 1)).eq(pattern); done(); } }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri]); }); // Try with an output size that exceeds the default node buffer size // (200 * 1024) when spawning a new process. it('should work with a lot of results', done => { // This can take a bit of time. this.timeout(150000); const pattern = 'lots-of-matches'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = []; for (let i = 1; i <= 100000; i++) { expected.push({ root: rootDirAUri, fileUri: 'lots-of-matches', line: i, character: 1, length: pattern.length, lineText: '', }); } compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri]); }); // Try limiting the number of returned results. it('should limit the number of returned results', done => { const pattern = 'lots-of-matches'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = []; for (let i = 1; i <= 1000; i++) { expected.push({ root: rootDirAUri, fileUri: 'lots-of-matches', line: i, character: 1, length: pattern.length, lineText: '', }); } compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri], { maxResults: 1000, }); }); // Try with regexes. it('should search for regexes', done => { const pattern = 'h[e3]l+[o0]'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'regexes', line: 1, character: 5, length: 5, lineText: '' }, { root: rootDirAUri, fileUri: 'regexes', line: 1, character: 14, length: 4, lineText: '' }, { root: rootDirAUri, fileUri: 'regexes', line: 1, character: 21, length: 5, lineText: '' }, { root: rootDirAUri, fileUri: 'regexes', line: 1, character: 26, length: 6, lineText: '' }, { root: rootDirAUri, fileUri: 'regexes', line: 2, character: 1, length: 5, lineText: '' }, ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri], { useRegExp: true }); }); // Try without regex it('should search for fixed string', done => { const pattern = 'hello.'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'regexes', line: 1, character: 5, length: 6, lineText: '' } ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri], { useRegExp: false }); }); // Try with a pattern starting with -, and in filenames containing colons and spaces. it('should search a pattern starting with -', done => { const pattern = '-fo+bar'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'file with spaces', line: 1, character: 28, length: 7, lineText: '' }, ]; if (!isWindows) { expected.push( { root: rootDirAUri, fileUri: 'file:with:some:colons', line: 1, character: 28, length: 7, lineText: '' } ); } compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri], { useRegExp: true }); }); // Try with a pattern starting with --, and in filenames containing colons and spaces. it('should search a pattern starting with --', done => { const pattern = '--fo+bar'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'file with spaces', line: 1, character: 27, length: 8, lineText: '' }, ]; if (!isWindows) { expected.push( { root: rootDirAUri, fileUri: 'file:with:some:colons', line: 1, character: 27, length: 8, lineText: '' } ); } compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri], { useRegExp: true }); }); it('should search a pattern starting with a dash w/o regex', done => { const pattern = '-foobar'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'file with spaces', line: 1, character: 28, length: 7, lineText: '' }, ]; if (!isWindows) { expected.push( { root: rootDirAUri, fileUri: 'file:with:some:colons', line: 1, character: 28, length: 7, lineText: '' } ); } compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri]); }); it('should search a pattern starting with two dashes w/o regex', done => { const pattern = '--foobar'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'file with spaces', line: 1, character: 27, length: 8, lineText: '' }, ]; if (!isWindows) { expected.push( { root: rootDirAUri, fileUri: 'file:with:some:colons', line: 1, character: 27, length: 8, lineText: '' } ); } compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri]); }); it('should search a whole pattern starting with - w/o regex', done => { const pattern = '-glob'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'glob', line: 1, character: 7, length: 5, lineText: '' }, { root: rootDirAUri, fileUri: 'glob.txt', line: 1, character: 6, length: 5, lineText: '' } ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri], { matchWholeWord: true }); }); it('should search a whole pattern starting with -- w/o regex', done => { const pattern = '--glob'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'glob', line: 1, character: 6, length: 6, lineText: '' } ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri], { matchWholeWord: true }); }); it('should search a pattern in .txt file', done => { const pattern = '-glob'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'glob.txt', line: 1, character: 6, length: 5, lineText: '' } ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri], { include: ['*.txt'] }); }); it('should search a whole pattern in .txt file', done => { const pattern = '-glob'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'glob.txt', line: 1, character: 6, length: 5, lineText: '' } ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri], { include: ['*.txt'], matchWholeWord: true }); }); it('should search in a given file by relative path', done => { const pattern = 'carrots'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'potatoes', line: 1, character: 18, length: pattern.length, lineText: '' } ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri], { include: ['./potatoes'], matchWholeWord: true }); }); it('should only apply to sub-folders of given include', done => { const pattern = 'Copyright'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'test/test-spec.ts', line: 1, character: 5, length: pattern.length, lineText: '' } ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); // Matching only the top 'test' folder and not any other 'test' subfolder ripgrepServer.search(pattern, [rootDirAUri], { include: ['./test'], matchWholeWord: true }); }); it('should apply to all sub-folders of not relative pattern', done => { const pattern = 'Copyright'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'small/test/test-spec.ts', line: 1, character: 5, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'test/test-spec.ts', line: 1, character: 5, length: pattern.length, lineText: '' } ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); // Matching only the top 'test' folder and not any other 'test' subfolder ripgrepServer.search(pattern, [rootDirAUri], { include: ['test'], matchWholeWord: true }); }); it('should consider "include" string as a file', done => { const pattern = 'slightly'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'orange', line: 1, character: 27, length: pattern.length, lineText: '' } ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirBUri], { include: ['orange'], matchWholeWord: true }); }); it('should consider "include" string as a folder', done => { const pattern = 'Most'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'orange/navel', line: 1, character: 5, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'orange/hamlin', line: 1, character: 33, length: pattern.length, lineText: '' } ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri], { include: ['orange'], matchWholeWord: true }); }); it('should return 1 result when searching for "test" while ignoring all ".txt" files', done => { const pattern = 'test'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'glob', line: 1, character: 1, length: pattern.length, lineText: '' }, ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri, rootDirBUri], { exclude: ['*.txt'] }); }); // Try searching in an UTF-8 file. it('should search in a UTF-8 file', done => { const pattern = ' jag'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'utf8-file', line: 1, character: 7, length: 4, lineText: '' }, { root: rootDirAUri, fileUri: 'utf8-file', line: 1, character: 23, length: 4, lineText: '' }, ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri]); }); // Try searching a pattern that contains unicode characters. it('should search a UTF-8 pattern', done => { const pattern = ' h?är'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'utf8-file', line: 1, character: 4, length: 3, lineText: '' }, { root: rootDirAUri, fileUri: 'utf8-file', line: 1, character: 20, length: 3, lineText: '' }, { root: rootDirAUri, fileUri: 'utf8-file', line: 1, character: 27, length: 4, lineText: '' }, ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri], { useRegExp: true }); }); // A regex that may match an empty string should not return zero-length // results. Run the test in a directory without big files, because it // makes rg print all searched lines, which can take a lot of time. it('should not return zero-length matches', done => { const pattern = '(hello)?'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri + '/small']); }); it('should search a pattern with special characters ', done => { const pattern = 'salut";\' echo foo && echo bar; "'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirAUri, fileUri: 'special shell characters', line: 1, character: 14, length: 32, lineText: '' }, ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri], { useRegExp: true }); }); it('should find patterns across all directories', done => { const pattern = 'carrot'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootDirBUri, fileUri: 'orange', line: 1, character: 51, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'carrots', line: 1, character: 11, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'carrots', line: 2, character: 6, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'carrots', line: 2, character: 35, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'carrots', line: 3, character: 28, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'carrots', line: 3, character: 52, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'carrots', line: 4, character: 1, length: pattern.length, lineText: '' }, { root: rootDirAUri, fileUri: 'potatoes', line: 1, character: 18, length: pattern.length, lineText: '' } ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri, rootDirBUri]); }); it('should only find patterns from the folder closest to the file', done => { const pattern = 'folder'; const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceExpectation[] = [ { root: rootSubdirAUri, fileUri: 'folderSubfolder', line: 1, character: 18, length: pattern.length, lineText: '' }, { root: rootSubdirAUri, fileUri: 'folderSubfolder', line: 1, character: 30, length: pattern.length, lineText: '' }, { root: rootSubdirAUri, fileUri: 'folderSubfolder', line: 1, character: 18, length: pattern.length, lineText: '' }, { root: rootSubdirAUri, fileUri: 'folderSubfolder', line: 1, character: 30, length: pattern.length, lineText: '' } ]; compareSearchResults(expected, client.results); done(); }); ripgrepServer.setClient(client); ripgrepServer.search(pattern, [rootDirAUri, rootSubdirAUri]); }); it('fails gracefully when rg isn\'t found', async function (): Promise { const errorString = await new Promise((resolve, reject) => { const rgServer = createInstance('/non-existent/rg'); rgServer.setClient({ onResult: (searchId: number, result: SearchInWorkspaceResult): void => { reject(); }, onDone: (searchId: number, error?: string): void => { if (typeof error === 'string') { resolve(error); } else { reject(); } }, }); rgServer.search('pattern', [rootDirA]); }); expect(errorString).contains('could not find the ripgrep (rg) binary'); }); it('fails gracefully when rg isn\'t executable', async function (): Promise { const errorString = await new Promise((resolve, reject) => { // Create temporary file, ensure it is not executable. const rg = temp.openSync(); let mode = fs.fstatSync(rg.fd).mode; mode &= ~(fs.constants.S_IXUSR | fs.constants.S_IXGRP | fs.constants.S_IXOTH); fs.fchmodSync(rg.fd, mode); fs.closeSync(rg.fd); const rgServer = createInstance(rg.path); rgServer.setClient({ onResult: (searchId: number, result: SearchInWorkspaceResult): void => { reject(); }, onDone: (searchId: number, error?: string): void => { if (typeof error === 'string') { resolve(error); } else { reject(); } }, }); rgServer.search('pattern', [rootDirA]); }); if (isWindows) { expect(errorString).contains('An error happened while searching'); } else { expect(errorString).contains('could not execute the ripgrep (rg) binary'); } }); }); describe('#extractSearchPathsFromIncludes', function (): void { this.timeout(10000); it('should not resolve paths from a not absolute / relative pattern', async () => { const pattern = 'carrots'; const options = { include: [pattern] }; const searchPaths = await ripgrepServer['extractSearchPathsFromIncludes']([rootDirA], options); // Same root directory expect(searchPaths.length).equal(1); expect(searchPaths[0]).equal(rootDirA); // Pattern is unchanged expect(options.include.length).equal(1); expect(options.include[0]).equals(pattern); }); it('should resolve pattern to path for relative filename', async () => { const filename = 'carrots'; const pattern = `./${filename}`; await checkResolvedPathForPattern(pattern, path.join(rootDirA, filename)); }); it('should resolve relative pattern with sub-folders glob', async () => { const filename = 'carrots'; const pattern = `./${filename}/**`; await checkResolvedPathForPattern(pattern, path.join(rootDirA, filename)); }); it('should resolve absolute path pattern', async () => { const pattern = `${rootDirA}/carrots`; await checkResolvedPathForPattern(pattern, pattern); }); }); describe('#addGlobArgs', function (): void { this.timeout(10000); it('should resolve path to glob - filename', function (): void { [true, false].forEach(excludeFlag => { const excludePrefix = excludeFlag ? '!' : ''; const filename = 'carrots'; const expected = [ `--glob=${excludePrefix}**/${filename}`, `--glob=${excludePrefix}**/${filename}/*` ]; const actual = new Set(); ripgrepServer['addGlobArgs'](actual, [filename], excludeFlag); expect(expected).to.have.deep.members([...actual]); }); }); it('should resolve path to glob - glob prefixed folder', function (): void { [true, false].forEach(excludeFlag => { const excludePrefix = excludeFlag ? '!' : ''; const filename = 'carrots'; const inputPath = `**/${filename}/`; const expected = [ `--glob=${excludePrefix}**/${filename}/`, `--glob=${excludePrefix}**/${filename}/*` ]; const actual = new Set(); ripgrepServer['addGlobArgs'](actual, [inputPath], excludeFlag); expect(expected).to.have.deep.members([...actual]); }); }); it('should resolve path to glob - path segment', function (): void { [true, false].forEach(excludeFlag => { const excludePrefix = excludeFlag ? '!' : ''; const filename = 'carrots'; const inputPath = `/${filename}`; const expected = [ `--glob=${excludePrefix}**/${filename}`, `--glob=${excludePrefix}**/${filename}/*` ]; const actual = new Set(); ripgrepServer['addGlobArgs'](actual, [inputPath], excludeFlag); expect(expected).to.have.deep.members([...actual]); }); }); it('should resolve path to glob - already a glob', function (): void { [true, false].forEach(excludeFlag => { const excludePrefix = excludeFlag ? '!' : ''; const filename = 'carrots'; const inputPath = `${filename}/**/*`; const expected = [ `--glob=${excludePrefix}**/${filename}/**/*`, ]; const actual = new Set(); ripgrepServer['addGlobArgs'](actual, [inputPath], excludeFlag); expect(expected).to.have.deep.members([...actual]); }); }); it('should resolve path to glob - path segment glob suffixed', function (): void { [true, false].forEach(excludeFlag => { const excludePrefix = excludeFlag ? '!' : ''; const filename = 'carrots'; const inputPath = `/${filename}/**/*`; const expected = [ `--glob=${excludePrefix}**/${filename}/**/*`, ]; const actual = new Set(); ripgrepServer['addGlobArgs'](actual, [inputPath], excludeFlag); expect(expected).to.have.deep.members([...actual]); }); }); }); async function checkResolvedPathForPattern(pattern: string, expectedPath: string): Promise { const options = { include: [pattern] }; const searchPaths = await ripgrepServer['extractSearchPathsFromIncludes']([rootDirA], options); expect(searchPaths.length).equal(1, 'searchPath result should contain exactly one element'); expect(options.include.length).equals(0, 'options.include should be empty'); expect(searchPaths[0]).equal(path.normalize(expectedPath)); }