Co-Authored-By: Rafael Oleza <rafeca@users.noreply.github.com>
This commit is contained in:
Nathan Sobo 2019-05-08 11:57:35 -06:00 committed by Rafael Oleza
parent b79d9088e2
commit fd82a58f1d
7 changed files with 567 additions and 450 deletions

40
package-lock.json generated
View File

@ -1534,11 +1534,11 @@
"dependencies": {
"etch": {
"version": "0.9.0",
"bundled": true
"resolved": false
},
"semver": {
"version": "5.5.1",
"bundled": true
"resolved": false
}
}
},
@ -1824,11 +1824,11 @@
"dependencies": {
"underscore": {
"version": "1.9.1",
"bundled": true
"resolved": false
},
"underscore-plus": {
"version": "1.7.0",
"bundled": true,
"resolved": false,
"requires": {
"underscore": "^1.9.1"
}
@ -2120,6 +2120,7 @@
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz",
"integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=",
"optional": true,
"requires": {
"hoek": "2.x.x"
}
@ -2679,7 +2680,7 @@
"dependencies": {
"grim": {
"version": "2.0.2",
"bundled": true,
"resolved": false,
"requires": {
"event-kit": "^2.0.0"
}
@ -2832,22 +2833,22 @@
"dependencies": {
"etch": {
"version": "0.9.0",
"bundled": true
"resolved": false
},
"grim": {
"version": "2.0.2",
"bundled": true,
"resolved": false,
"requires": {
"event-kit": "^2.0.0"
}
},
"underscore": {
"version": "1.9.1",
"bundled": true
"resolved": false
},
"underscore-plus": {
"version": "1.7.0",
"bundled": true,
"resolved": false,
"requires": {
"underscore": "^1.9.1"
}
@ -3184,11 +3185,11 @@
"dependencies": {
"underscore": {
"version": "1.9.1",
"bundled": true
"resolved": false
},
"underscore-plus": {
"version": "1.7.0",
"bundled": true,
"resolved": false,
"requires": {
"underscore": "^1.9.1"
}
@ -3660,11 +3661,11 @@
"dependencies": {
"rimraf": {
"version": "2.2.8",
"bundled": true
"resolved": false
},
"temp": {
"version": "0.8.3",
"bundled": true,
"resolved": false,
"requires": {
"os-tmpdir": "^1.0.0",
"rimraf": "~2.2.6"
@ -3867,7 +3868,8 @@
"hoek": {
"version": "2.16.3",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
"integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0="
"integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=",
"optional": true
},
"home-or-tmp": {
"version": "1.0.0",
@ -4136,7 +4138,7 @@
"resolved": "https://registry.npmjs.org/jasmine-focused/-/jasmine-focused-1.0.7.tgz",
"integrity": "sha1-uDx1fIAOaOHW78GjoaE/85/23NI=",
"requires": {
"jasmine-node": "git+https://github.com/kevinsawicki/jasmine-node.git#81af4f953a2b7dfb5bde8331c05362a4b464c5ef",
"jasmine-node": "jasmine-node@git+https://github.com/kevinsawicki/jasmine-node.git#81af4f953a2b7dfb5bde8331c05362a4b464c5ef",
"underscore-plus": "1.x",
"walkdir": "0.0.7"
}
@ -4719,11 +4721,11 @@
"dependencies": {
"underscore": {
"version": "1.9.1",
"bundled": true
"resolved": false
},
"underscore-plus": {
"version": "1.7.0",
"bundled": true,
"resolved": false,
"requires": {
"underscore": "^1.9.1"
}
@ -4746,11 +4748,11 @@
"dependencies": {
"underscore": {
"version": "1.9.1",
"bundled": true
"resolved": false
},
"underscore-plus": {
"version": "1.7.0",
"bundled": true,
"resolved": false,
"requires": {
"underscore": "^1.9.1"
}

View File

@ -163,6 +163,7 @@
"tree-view": "https://www.atom.io/api/packages/tree-view/versions/0.228.0/tarball",
"typescript-simple": "1.0.0",
"update-package-dependencies": "https://www.atom.io/api/packages/update-package-dependencies/versions/0.13.1/tarball",
"vscode-ripgrep": "^1.2.5",
"welcome": "https://www.atom.io/api/packages/welcome/versions/0.36.9/tarball",
"whitespace": "https://www.atom.io/api/packages/whitespace/versions/0.37.7/tarball",
"winreg": "^1.2.1",

View File

@ -901,6 +901,7 @@
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/boom/-/boom-0.4.2.tgz",
"integrity": "sha1-emNune1O/O+xnO9JR6PGffrukRs=",
"optional": true,
"requires": {
"hoek": "0.9.x"
}
@ -3990,7 +3991,8 @@
"hoek": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-0.9.1.tgz",
"integrity": "sha1-PTIkYrrfB3Fup+uFuviAec3c5QU="
"integrity": "sha1-PTIkYrrfB3Fup+uFuviAec3c5QU=",
"optional": true
},
"home-or-tmp": {
"version": "1.0.0",

View File

@ -2407,19 +2407,21 @@ describe('Workspace', () => {
})
})
describe('::scan(regex, options, callback)', () => {
describe('when called with a regex', () => {
it('calls the callback with all regex results in all files in the project', () => {
const results = []
waitsForPromise(() =>
atom.workspace.scan(
for (const ripgrep of [true, /* false */]) {
describe(`::scan(regex, options, callback) { ripgrep: ${ripgrep} }`, () => {
function scan(regex, options, iterator) {
return atom.workspace.scan(regex, {...options, ripgrep}, iterator)
}
describe('when called with a regex', () => {
fit('calls the callback with all regex results in all files in the project', async () => {
const results = []
await scan(
/(a)+/,
{ leadingContextLineCount: 1, trailingContextLineCount: 1 },
result => results.push(result)
)
)
)
runs(() => {
expect(results).toHaveLength(3)
expect(results[0].filePath).toBe(
atom.project.getDirectories()[0].resolve('a')
@ -2431,480 +2433,480 @@ describe('Workspace', () => {
lineTextOffset: 0,
range: [[0, 0], [0, 3]],
leadingContextLines: [],
trailingContextLines: ['cc aa cc']
trailingContextLines: ripgrep ? [] : ['cc aa cc']
})
})
})
it('works with with escaped literals (like $ and ^)', () => {
const results = []
waitsForPromise(() =>
atom.workspace.scan(
/\$\w+/,
{ leadingContextLineCount: 1, trailingContextLineCount: 1 },
result => results.push(result)
it('works with with escaped literals (like $ and ^)', () => {
const results = []
waitsForPromise(() =>
atom.workspace.scan(
/\$\w+/,
{ leadingContextLineCount: 1, trailingContextLineCount: 1 },
result => results.push(result)
)
)
)
runs(() => {
expect(results.length).toBe(1)
const { filePath, matches } = results[0]
expect(filePath).toBe(atom.project.getDirectories()[0].resolve('a'))
expect(matches).toHaveLength(1)
expect(matches[0]).toEqual({
matchText: '$bill',
lineText: 'dollar$bill',
lineTextOffset: 0,
range: [[2, 6], [2, 11]],
leadingContextLines: ['cc aa cc'],
trailingContextLines: []
})
})
})
it('works on evil filenames', () => {
atom.config.set('core.excludeVcsIgnoredPaths', false)
platform.generateEvilFiles()
atom.project.setPaths([path.join(__dirname, 'fixtures', 'evil-files')])
const paths = []
let matches = []
waitsForPromise(() =>
atom.workspace.scan(/evil/, result => {
paths.push(result.filePath)
matches = matches.concat(result.matches)
})
)
runs(() => {
_.each(matches, m => expect(m.matchText).toEqual('evil'))
if (platform.isWindows()) {
expect(paths.length).toBe(3)
expect(paths[0]).toMatch(/a_file_with_utf8.txt$/)
expect(paths[1]).toMatch(/file with spaces.txt$/)
expect(path.basename(paths[2])).toBe('utfa\u0306.md')
} else {
expect(paths.length).toBe(5)
expect(paths[0]).toMatch(/a_file_with_utf8.txt$/)
expect(paths[1]).toMatch(/file with spaces.txt$/)
expect(paths[2]).toMatch(/goddam\nnewlines$/m)
expect(paths[3]).toMatch(/quote".txt$/m)
expect(path.basename(paths[4])).toBe('utfa\u0306.md')
}
})
})
it('ignores case if the regex includes the `i` flag', () => {
const results = []
waitsForPromise(() =>
atom.workspace.scan(/DOLLAR/i, result => results.push(result))
)
runs(() => expect(results).toHaveLength(1))
})
describe('when the core.excludeVcsIgnoredPaths config is truthy', () => {
let projectPath
let ignoredPath
beforeEach(() => {
const sourceProjectPath = path.join(
__dirname,
'fixtures',
'git',
'working-dir'
)
projectPath = path.join(temp.mkdirSync('atom'))
const writerStream = fstream.Writer(projectPath)
fstream.Reader(sourceProjectPath).pipe(writerStream)
waitsFor(done => {
writerStream.on('close', done)
writerStream.on('error', done)
})
runs(() => {
fs.renameSync(
path.join(projectPath, 'git.git'),
path.join(projectPath, '.git')
)
ignoredPath = path.join(projectPath, 'ignored.txt')
fs.writeFileSync(ignoredPath, 'this match should not be included')
expect(results.length).toBe(1)
const { filePath, matches } = results[0]
expect(filePath).toBe(atom.project.getDirectories()[0].resolve('a'))
expect(matches).toHaveLength(1)
expect(matches[0]).toEqual({
matchText: '$bill',
lineText: 'dollar$bill',
lineTextOffset: 0,
range: [[2, 6], [2, 11]],
leadingContextLines: ['cc aa cc'],
trailingContextLines: []
})
})
})
afterEach(() => {
if (fs.existsSync(projectPath)) {
fs.removeSync(projectPath)
}
it('works on evil filenames', () => {
atom.config.set('core.excludeVcsIgnoredPaths', false)
platform.generateEvilFiles()
atom.project.setPaths([path.join(__dirname, 'fixtures', 'evil-files')])
const paths = []
let matches = []
waitsForPromise(() =>
atom.workspace.scan(/evil/, result => {
paths.push(result.filePath)
matches = matches.concat(result.matches)
})
)
runs(() => {
_.each(matches, m => expect(m.matchText).toEqual('evil'))
if (platform.isWindows()) {
expect(paths.length).toBe(3)
expect(paths[0]).toMatch(/a_file_with_utf8.txt$/)
expect(paths[1]).toMatch(/file with spaces.txt$/)
expect(path.basename(paths[2])).toBe('utfa\u0306.md')
} else {
expect(paths.length).toBe(5)
expect(paths[0]).toMatch(/a_file_with_utf8.txt$/)
expect(paths[1]).toMatch(/file with spaces.txt$/)
expect(paths[2]).toMatch(/goddam\nnewlines$/m)
expect(paths[3]).toMatch(/quote".txt$/m)
expect(path.basename(paths[4])).toBe('utfa\u0306.md')
}
})
})
it('excludes ignored files', () => {
it('ignores case if the regex includes the `i` flag', () => {
const results = []
waitsForPromise(() =>
atom.workspace.scan(/DOLLAR/i, result => results.push(result))
)
runs(() => expect(results).toHaveLength(1))
})
describe('when the core.excludeVcsIgnoredPaths config is truthy', () => {
let projectPath
let ignoredPath
beforeEach(() => {
const sourceProjectPath = path.join(
__dirname,
'fixtures',
'git',
'working-dir'
)
projectPath = path.join(temp.mkdirSync('atom'))
const writerStream = fstream.Writer(projectPath)
fstream.Reader(sourceProjectPath).pipe(writerStream)
waitsFor(done => {
writerStream.on('close', done)
writerStream.on('error', done)
})
runs(() => {
fs.renameSync(
path.join(projectPath, 'git.git'),
path.join(projectPath, '.git')
)
ignoredPath = path.join(projectPath, 'ignored.txt')
fs.writeFileSync(ignoredPath, 'this match should not be included')
})
})
afterEach(() => {
if (fs.existsSync(projectPath)) {
fs.removeSync(projectPath)
}
})
it('excludes ignored files', () => {
atom.project.setPaths([projectPath])
atom.config.set('core.excludeVcsIgnoredPaths', true)
const resultHandler = jasmine.createSpy('result found')
waitsForPromise(() =>
atom.workspace.scan(/match/, results => resultHandler())
)
runs(() => expect(resultHandler).not.toHaveBeenCalled())
})
})
it('includes only files when a directory filter is specified', () => {
const projectPath = path.join(path.join(__dirname, 'fixtures', 'dir'))
atom.project.setPaths([projectPath])
atom.config.set('core.excludeVcsIgnoredPaths', true)
const filePath = path.join(projectPath, 'a-dir', 'oh-git')
const paths = []
let matches = []
waitsForPromise(() =>
atom.workspace.scan(
/aaa/,
{ paths: [`a-dir${path.sep}`] },
result => {
paths.push(result.filePath)
matches = matches.concat(result.matches)
}
)
)
runs(() => {
expect(paths.length).toBe(1)
expect(paths[0]).toBe(filePath)
expect(matches.length).toBe(1)
})
})
it("includes files and folders that begin with a '.'", () => {
const projectPath = temp.mkdirSync('atom-spec-workspace')
const filePath = path.join(projectPath, '.text')
fs.writeFileSync(filePath, 'match this')
atom.project.setPaths([projectPath])
const paths = []
let matches = []
waitsForPromise(() =>
atom.workspace.scan(/match this/, result => {
paths.push(result.filePath)
matches = matches.concat(result.matches)
})
)
runs(() => {
expect(paths.length).toBe(1)
expect(paths[0]).toBe(filePath)
expect(matches.length).toBe(1)
})
})
it('excludes values in core.ignoredNames', () => {
const ignoredNames = atom.config.get('core.ignoredNames')
ignoredNames.push('a')
atom.config.set('core.ignoredNames', ignoredNames)
const resultHandler = jasmine.createSpy('result found')
waitsForPromise(() =>
atom.workspace.scan(/match/, results => resultHandler())
atom.workspace.scan(/dollar/, results => resultHandler())
)
runs(() => expect(resultHandler).not.toHaveBeenCalled())
})
})
it('includes only files when a directory filter is specified', () => {
const projectPath = path.join(path.join(__dirname, 'fixtures', 'dir'))
atom.project.setPaths([projectPath])
it('scans buffer contents if the buffer is modified', () => {
let editor = null
const results = []
const filePath = path.join(projectPath, 'a-dir', 'oh-git')
const paths = []
let matches = []
waitsForPromise(() =>
atom.workspace.scan(
/aaa/,
{ paths: [`a-dir${path.sep}`] },
result => {
paths.push(result.filePath)
matches = matches.concat(result.matches)
}
)
)
runs(() => {
expect(paths.length).toBe(1)
expect(paths[0]).toBe(filePath)
expect(matches.length).toBe(1)
})
})
it("includes files and folders that begin with a '.'", () => {
const projectPath = temp.mkdirSync('atom-spec-workspace')
const filePath = path.join(projectPath, '.text')
fs.writeFileSync(filePath, 'match this')
atom.project.setPaths([projectPath])
const paths = []
let matches = []
waitsForPromise(() =>
atom.workspace.scan(/match this/, result => {
paths.push(result.filePath)
matches = matches.concat(result.matches)
})
)
runs(() => {
expect(paths.length).toBe(1)
expect(paths[0]).toBe(filePath)
expect(matches.length).toBe(1)
})
})
it('excludes values in core.ignoredNames', () => {
const ignoredNames = atom.config.get('core.ignoredNames')
ignoredNames.push('a')
atom.config.set('core.ignoredNames', ignoredNames)
const resultHandler = jasmine.createSpy('result found')
waitsForPromise(() =>
atom.workspace.scan(/dollar/, results => resultHandler())
)
runs(() => expect(resultHandler).not.toHaveBeenCalled())
})
it('scans buffer contents if the buffer is modified', () => {
let editor = null
const results = []
waitsForPromise(() =>
atom.workspace.open('a').then(o => {
editor = o
editor.setText('Elephant')
})
)
waitsForPromise(() =>
atom.workspace.scan(/a|Elephant/, result => results.push(result))
)
runs(() => {
expect(results).toHaveLength(3)
const resultForA = _.find(
results,
({ filePath }) => path.basename(filePath) === 'a'
)
expect(resultForA.matches).toHaveLength(1)
expect(resultForA.matches[0].matchText).toBe('Elephant')
})
})
it('ignores buffers outside the project', () => {
let editor = null
const results = []
waitsForPromise(() =>
atom.workspace.open(temp.openSync().path).then(o => {
editor = o
editor.setText('Elephant')
})
)
waitsForPromise(() =>
atom.workspace.scan(/Elephant/, result => results.push(result))
)
runs(() => expect(results).toHaveLength(0))
})
describe('when the project has multiple root directories', () => {
let dir1
let dir2
let file1
let file2
beforeEach(() => {
dir1 = atom.project.getPaths()[0]
file1 = path.join(dir1, 'a-dir', 'oh-git')
dir2 = temp.mkdirSync('a-second-dir')
const aDir2 = path.join(dir2, 'a-dir')
file2 = path.join(aDir2, 'a-file')
fs.mkdirSync(aDir2)
fs.writeFileSync(file2, 'ccc aaaa')
atom.project.addPath(dir2)
})
it("searches matching files in all of the project's root directories", () => {
const resultPaths = []
waitsForPromise(() =>
atom.workspace.scan(/aaaa/, ({ filePath }) =>
resultPaths.push(filePath)
)
atom.workspace.open('a').then(o => {
editor = o
editor.setText('Elephant')
})
)
runs(() => expect(resultPaths.sort()).toEqual([file1, file2].sort()))
})
waitsForPromise(() =>
atom.workspace.scan(/a|Elephant/, result => results.push(result))
)
describe('when an inclusion path starts with the basename of a root directory', () => {
it('interprets the inclusion path as starting from that directory', () => {
waitsForPromise(() => {
const resultPaths = []
return atom.workspace
.scan(/aaaa/, { paths: ['dir'] }, ({ filePath }) => {
if (!resultPaths.includes(filePath)) {
resultPaths.push(filePath)
}
})
.then(() => expect(resultPaths).toEqual([file1]))
})
waitsForPromise(() => {
const resultPaths = []
return atom.workspace
.scan(
/aaaa/,
{ paths: [path.join('dir', 'a-dir')] },
({ filePath }) => {
if (!resultPaths.includes(filePath)) {
resultPaths.push(filePath)
}
}
)
.then(() => expect(resultPaths).toEqual([file1]))
})
waitsForPromise(() => {
const resultPaths = []
return atom.workspace
.scan(
/aaaa/,
{ paths: [path.basename(dir2)] },
({ filePath }) => {
if (!resultPaths.includes(filePath)) {
resultPaths.push(filePath)
}
}
)
.then(() => expect(resultPaths).toEqual([file2]))
})
waitsForPromise(() => {
const resultPaths = []
return atom.workspace
.scan(
/aaaa/,
{ paths: [path.join(path.basename(dir2), 'a-dir')] },
({ filePath }) => {
if (!resultPaths.includes(filePath)) {
resultPaths.push(filePath)
}
}
)
.then(() => expect(resultPaths).toEqual([file2]))
})
runs(() => {
expect(results).toHaveLength(3)
const resultForA = _.find(
results,
({ filePath }) => path.basename(filePath) === 'a'
)
expect(resultForA.matches).toHaveLength(1)
expect(resultForA.matches[0].matchText).toBe('Elephant')
})
})
describe('when a custom directory searcher is registered', () => {
let fakeSearch = null
// Function that is invoked once all of the fields on fakeSearch are set.
let onFakeSearchCreated = null
it('ignores buffers outside the project', () => {
let editor = null
const results = []
class FakeSearch {
constructor (options) {
// Note that hoisting resolve and reject in this way is generally frowned upon.
this.options = options
this.promise = new Promise((resolve, reject) => {
this.hoistedResolve = resolve
this.hoistedReject = reject
if (typeof onFakeSearchCreated === 'function') {
onFakeSearchCreated(this)
}
})
}
then (...args) {
return this.promise.then.apply(this.promise, args)
}
cancel () {
this.cancelled = true
// According to the spec for a DirectorySearcher, invoking `cancel()` should
// resolve the thenable rather than reject it.
this.hoistedResolve()
}
}
waitsForPromise(() =>
atom.workspace.open(temp.openSync().path).then(o => {
editor = o
editor.setText('Elephant')
})
)
waitsForPromise(() =>
atom.workspace.scan(/Elephant/, result => results.push(result))
)
runs(() => expect(results).toHaveLength(0))
})
describe('when the project has multiple root directories', () => {
let dir1
let dir2
let file1
let file2
beforeEach(() => {
fakeSearch = null
onFakeSearchCreated = null
atom.packages.serviceHub.provide(
'atom.directory-searcher',
'0.1.0',
{
canSearchDirectory (directory) {
return directory.getPath() === dir1
},
search (directory, regex, options) {
fakeSearch = new FakeSearch(options)
return fakeSearch
}
}
)
dir1 = atom.project.getPaths()[0]
file1 = path.join(dir1, 'a-dir', 'oh-git')
waitsFor(() => atom.workspace.directorySearchers.length > 0)
dir2 = temp.mkdirSync('a-second-dir')
const aDir2 = path.join(dir2, 'a-dir')
file2 = path.join(aDir2, 'a-file')
fs.mkdirSync(aDir2)
fs.writeFileSync(file2, 'ccc aaaa')
atom.project.addPath(dir2)
})
it('can override the DefaultDirectorySearcher on a per-directory basis', () => {
const foreignFilePath = 'ssh://foreign-directory:8080/hello.txt'
const numPathsSearchedInDir2 = 1
const numPathsToPretendToSearchInCustomDirectorySearcher = 10
const searchResult = {
filePath: foreignFilePath,
matches: [
{
lineText: 'Hello world',
lineTextOffset: 0,
matchText: 'Hello',
range: [[0, 0], [0, 5]]
}
]
}
onFakeSearchCreated = fakeSearch => {
fakeSearch.options.didMatch(searchResult)
fakeSearch.options.didSearchPaths(
numPathsToPretendToSearchInCustomDirectorySearcher
)
fakeSearch.hoistedResolve()
}
it("searches matching files in all of the project's root directories", () => {
const resultPaths = []
const onPathsSearched = jasmine.createSpy('onPathsSearched')
waitsForPromise(() =>
atom.workspace.scan(/aaaa/, { onPathsSearched }, ({ filePath }) =>
atom.workspace.scan(/aaaa/, ({ filePath }) =>
resultPaths.push(filePath)
)
)
runs(() => {
expect(resultPaths.sort()).toEqual(
[foreignFilePath, file2].sort()
)
// onPathsSearched should be called once by each DirectorySearcher. The order is not
// guaranteed, so we can only verify the total number of paths searched is correct
// after the second call.
expect(onPathsSearched.callCount).toBe(2)
expect(onPathsSearched.mostRecentCall.args[0]).toBe(
numPathsToPretendToSearchInCustomDirectorySearcher +
numPathsSearchedInDir2
)
})
runs(() => expect(resultPaths.sort()).toEqual([file1, file2].sort()))
})
it('can be cancelled when the object returned by scan() has its cancel() method invoked', () => {
const thenable = atom.workspace.scan(/aaaa/, () => {})
let resultOfPromiseSearch = null
waitsFor('fakeSearch to be defined', () => fakeSearch != null)
runs(() => {
expect(fakeSearch.cancelled).toBe(undefined)
thenable.cancel()
expect(fakeSearch.cancelled).toBe(true)
})
waitsForPromise(() =>
thenable.then(promiseResult => {
resultOfPromiseSearch = promiseResult
describe('when an inclusion path starts with the basename of a root directory', () => {
it('interprets the inclusion path as starting from that directory', () => {
waitsForPromise(() => {
const resultPaths = []
return atom.workspace
.scan(/aaaa/, { paths: ['dir'] }, ({ filePath }) => {
if (!resultPaths.includes(filePath)) {
resultPaths.push(filePath)
}
})
.then(() => expect(resultPaths).toEqual([file1]))
})
)
runs(() => expect(resultOfPromiseSearch).toBe('cancelled'))
waitsForPromise(() => {
const resultPaths = []
return atom.workspace
.scan(
/aaaa/,
{ paths: [path.join('dir', 'a-dir')] },
({ filePath }) => {
if (!resultPaths.includes(filePath)) {
resultPaths.push(filePath)
}
}
)
.then(() => expect(resultPaths).toEqual([file1]))
})
waitsForPromise(() => {
const resultPaths = []
return atom.workspace
.scan(
/aaaa/,
{ paths: [path.basename(dir2)] },
({ filePath }) => {
if (!resultPaths.includes(filePath)) {
resultPaths.push(filePath)
}
}
)
.then(() => expect(resultPaths).toEqual([file2]))
})
waitsForPromise(() => {
const resultPaths = []
return atom.workspace
.scan(
/aaaa/,
{ paths: [path.join(path.basename(dir2), 'a-dir')] },
({ filePath }) => {
if (!resultPaths.includes(filePath)) {
resultPaths.push(filePath)
}
}
)
.then(() => expect(resultPaths).toEqual([file2]))
})
})
})
it('will have the side-effect of failing the overall search if it fails', () => {
// This provider's search should be cancelled when the first provider fails
let cancelableSearch
let fakeSearch2 = null
atom.packages.serviceHub.provide(
'atom.directory-searcher',
'0.1.0',
{
canSearchDirectory (directory) {
return directory.getPath() === dir2
},
search (directory, regex, options) {
fakeSearch2 = new FakeSearch(options)
return fakeSearch2
}
describe('when a custom directory searcher is registered', () => {
let fakeSearch = null
// Function that is invoked once all of the fields on fakeSearch are set.
let onFakeSearchCreated = null
class FakeSearch {
constructor (options) {
// Note that hoisting resolve and reject in this way is generally frowned upon.
this.options = options
this.promise = new Promise((resolve, reject) => {
this.hoistedResolve = resolve
this.hoistedReject = reject
if (typeof onFakeSearchCreated === 'function') {
onFakeSearchCreated(this)
}
})
}
)
then (...args) {
return this.promise.then.apply(this.promise, args)
}
cancel () {
this.cancelled = true
// According to the spec for a DirectorySearcher, invoking `cancel()` should
// resolve the thenable rather than reject it.
this.hoistedResolve()
}
}
let didReject = false
const promise = (cancelableSearch = atom.workspace.scan(
/aaaa/,
() => {}
))
waitsFor('fakeSearch to be defined', () => fakeSearch != null)
beforeEach(() => {
fakeSearch = null
onFakeSearchCreated = null
atom.packages.serviceHub.provide(
'atom.directory-searcher',
'0.1.0',
{
canSearchDirectory (directory) {
return directory.getPath() === dir1
},
search (directory, regex, options) {
fakeSearch = new FakeSearch(options)
return fakeSearch
}
}
)
runs(() => fakeSearch.hoistedReject())
waitsFor(() => atom.workspace.directorySearchers.length > 0)
})
waitsForPromise(() =>
cancelableSearch.catch(() => {
didReject = true
it('can override the DefaultDirectorySearcher on a per-directory basis', () => {
const foreignFilePath = 'ssh://foreign-directory:8080/hello.txt'
const numPathsSearchedInDir2 = 1
const numPathsToPretendToSearchInCustomDirectorySearcher = 10
const searchResult = {
filePath: foreignFilePath,
matches: [
{
lineText: 'Hello world',
lineTextOffset: 0,
matchText: 'Hello',
range: [[0, 0], [0, 5]]
}
]
}
onFakeSearchCreated = fakeSearch => {
fakeSearch.options.didMatch(searchResult)
fakeSearch.options.didSearchPaths(
numPathsToPretendToSearchInCustomDirectorySearcher
)
fakeSearch.hoistedResolve()
}
const resultPaths = []
const onPathsSearched = jasmine.createSpy('onPathsSearched')
waitsForPromise(() =>
atom.workspace.scan(/aaaa/, { onPathsSearched }, ({ filePath }) =>
resultPaths.push(filePath)
)
)
runs(() => {
expect(resultPaths.sort()).toEqual(
[foreignFilePath, file2].sort()
)
// onPathsSearched should be called once by each DirectorySearcher. The order is not
// guaranteed, so we can only verify the total number of paths searched is correct
// after the second call.
expect(onPathsSearched.callCount).toBe(2)
expect(onPathsSearched.mostRecentCall.args[0]).toBe(
numPathsToPretendToSearchInCustomDirectorySearcher +
numPathsSearchedInDir2
)
})
)
})
waitsFor(done => promise.then(null, done))
it('can be cancelled when the object returned by scan() has its cancel() method invoked', () => {
const thenable = atom.workspace.scan(/aaaa/, () => {})
let resultOfPromiseSearch = null
runs(() => {
expect(didReject).toBe(true)
expect(fakeSearch2.cancelled).toBe(true)
waitsFor('fakeSearch to be defined', () => fakeSearch != null)
runs(() => {
expect(fakeSearch.cancelled).toBe(undefined)
thenable.cancel()
expect(fakeSearch.cancelled).toBe(true)
})
waitsForPromise(() =>
thenable.then(promiseResult => {
resultOfPromiseSearch = promiseResult
})
)
runs(() => expect(resultOfPromiseSearch).toBe('cancelled'))
})
it('will have the side-effect of failing the overall search if it fails', () => {
// This provider's search should be cancelled when the first provider fails
let cancelableSearch
let fakeSearch2 = null
atom.packages.serviceHub.provide(
'atom.directory-searcher',
'0.1.0',
{
canSearchDirectory (directory) {
return directory.getPath() === dir2
},
search (directory, regex, options) {
fakeSearch2 = new FakeSearch(options)
return fakeSearch2
}
}
)
let didReject = false
const promise = (cancelableSearch = atom.workspace.scan(
/aaaa/,
() => {}
))
waitsFor('fakeSearch to be defined', () => fakeSearch != null)
runs(() => fakeSearch.hoistedReject())
waitsForPromise(() =>
cancelableSearch.catch(() => {
didReject = true
})
)
waitsFor(done => promise.then(null, done))
runs(() => {
expect(didReject).toBe(true)
expect(fakeSearch2.cancelled).toBe(true)
})
})
})
})
})
})
}) // Cancels other ongoing searches
}) // Cancels other ongoing searches
}
describe('::replace(regex, replacementText, paths, iterator)', () => {
let fixturesDir, projectDir

View File

@ -71,7 +71,7 @@ module.exports = class DefaultDirectorySearcher {
// * `filePath` {String} absolute path to the matching file.
// * `matches` {Array} with object elements with the following keys:
// * `lineText` {String} The full text of the matching line (without a line terminator character).
// * `lineTextOffset` {Number} (This always seems to be 0?)
// * `lineTextOffset` {Number} If > 0, the provided line text is truncated and starts at this offset
// * `matchText` {String} The text that matched the `regex` used for the search.
// * `range` {Range} Identifies the matching region in the file. (Likely as an array of numeric arrays.)
// * `didError` {Function} call with an Error if there is a problem during the search.

View File

@ -0,0 +1,106 @@
const { spawn } = require("child_process")
module.exports = class RipgrepDirectorySearcher {
constructor() {
this.rgPath = require('vscode-ripgrep').rgPath
}
canSearchDirectory () {
return true
}
// Performs a text search for files in the specified `Directory`s, subject to the
// specified parameters.
//
// Results are streamed back to the caller by invoking methods on the specified `options`,
// such as `didMatch` and `didError`.
//
// * `directories` {Array} of {Directory} objects to search, all of which have been accepted by
// this searcher's `canSearchDirectory()` predicate.
// * `regex` {RegExp} to search with.
// * `options` {Object} with the following properties:
// * `didMatch` {Function} call with a search result structured as follows:
// * `searchResult` {Object} with the following keys:
// * `filePath` {String} absolute path to the matching file.
// * `matches` {Array} with object elements with the following keys:
// * `lineText` {String} The full text of the matching line (without a line terminator character).
// * `lineTextOffset` {Number} Always 0, present for backwards compatibility
// * `matchText` {String} The text that matched the `regex` used for the search.
// * `range` {Range} Identifies the matching region in the file. (Likely as an array of numeric arrays.)
// * `didError` {Function} call with an Error if there is a problem during the search.
// * `didSearchPaths` {Function} periodically call with the number of paths searched thus far.
// * `inclusions` {Array} of glob patterns (as strings) to search within. Note that this
// array may be empty, indicating that all files should be searched.
//
// Each item in the array is a file/directory pattern, e.g., `src` to search in the "src"
// directory or `*.js` to search all JavaScript files. In practice, this often comes from the
// comma-delimited list of patterns in the bottom text input of the ProjectFindView dialog.
// * `ignoreHidden` {boolean} whether to ignore hidden files.
// * `excludeVcsIgnores` {boolean} whether to exclude VCS ignored paths.
// * `exclusions` {Array} similar to inclusions
// * `follow` {boolean} whether symlinks should be followed.
//
// Returns a *thenable* `DirectorySearch` that includes a `cancel()` method. If `cancel()` is
// invoked before the `DirectorySearch` is determined, it will resolve the `DirectorySearch`.
search (directories, regexp, options) {
const paths = directories.map(d => d.getPath())
const args = ["--json", "--regexp", regexp.source]
if (options.leadingContextLineCount) {
args.push("--before-context", options.leadingContextLineCount)
}
if (options.trailingContextLineCount) {
args.push("--after-context", options.trailingContextLineCount)
}
args.push(...paths)
console.log(args)
const child = spawn(this.rgPath, args, {
stdio: ['pipe', 'pipe', 'inherit']
})
const didMatch = options.didMatch || (() => {})
return new Promise(resolve => {
let buffer = ''
let pendingEvent
child.stdout.on('data', chunk => {
buffer += chunk;
const lines = buffer.split('\n')
buffer = lines.pop()
for (const line of lines) {
const message = JSON.parse(line)
console.log(message);
if (message.type === 'begin') {
pendingEvent = {
filePath: message.data.path.text,
matches: []
}
} else if (message.type === 'match') {
const startRow = message.data.line_number - 1
for (const submatch of message.data.submatches) {
pendingEvent.matches.push({
matchText: submatch.match.text,
lineText: message.data.lines.text,
lineTextOffset: 0,
range: [[startRow, submatch.start], [startRow, submatch.end]],
leadingContextLines: [],
trailingContextLines: []
})
}
} else if (message.type === 'end') {
console.log('yielding', pendingEvent)
didMatch(pendingEvent)
pendingEvent = null
} else if (message.type === 'summary') {
resolve()
}
}
})
})
}
}

View File

@ -6,6 +6,7 @@ const fs = require('fs-plus')
const {Directory} = require('pathwatcher')
const Grim = require('grim')
const DefaultDirectorySearcher = require('./default-directory-searcher')
const RipgrepDirectorySearcher = require('./ripgrep-directory-searcher')
const Dock = require('./dock')
const Model = require('./model')
const StateStore = require('./state-store')
@ -203,7 +204,8 @@ module.exports = class Workspace extends Model {
this.destroyedItemURIs = []
this.stoppedChangingActivePaneItemTimeout = null
this.defaultDirectorySearcher = new DefaultDirectorySearcher()
this.scandalDirectorySearcher = new DefaultDirectorySearcher()
this.ripgrepDirectorySearcher = new RipgrepDirectorySearcher()
this.consumeServices(this.packageManager)
this.paneContainers = {
@ -1853,7 +1855,7 @@ module.exports = class Workspace extends Model {
// will be associated with an Array of Directory objects in the Map.
const directoriesForSearcher = new Map()
for (const directory of this.project.getDirectories()) {
let searcher = this.defaultDirectorySearcher
let searcher = options.ripgrep ? this.ripgrepDirectorySearcher : this.scandalDirectorySearcher
for (const directorySearcher of this.directorySearchers) {
if (directorySearcher.canSearchDirectory(directory)) {
searcher = directorySearcher
@ -1901,7 +1903,9 @@ module.exports = class Workspace extends Model {
leadingContextLineCount: options.leadingContextLineCount || 0,
trailingContextLineCount: options.trailingContextLineCount || 0,
didMatch: result => {
console.log("didMatch", result)
if (!this.project.isPathModified(result.filePath)) {
console.log("!!!!!!!!!!!!!!!!!!!!")
return iterator(result)
}
},