diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index b05543ad1..0583a0749 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -4,7 +4,6 @@ Project = require '../src/project' _ = require 'underscore-plus' fs = require 'fs-plus' path = require 'path' -platform = require './spec-helper-platform' BufferedProcess = require '../src/buffered-process' describe "Project", -> @@ -195,290 +194,6 @@ describe "Project", -> expect(atom.project.getPaths()[0]).toEqual path.dirname(require.resolve('./fixtures/dir/a')) expect(atom.project.getDirectories()[0].path).toEqual path.dirname(require.resolve('./fixtures/dir/a')) - describe ".replace()", -> - [filePath, commentFilePath, sampleContent, sampleCommentContent] = [] - - beforeEach -> - atom.project.setPaths([atom.project.resolve('../')]) - - filePath = atom.project.resolve('sample.js') - commentFilePath = atom.project.resolve('sample-with-comments.js') - sampleContent = fs.readFileSync(filePath).toString() - sampleCommentContent = fs.readFileSync(commentFilePath).toString() - - afterEach -> - fs.writeFileSync(filePath, sampleContent) - fs.writeFileSync(commentFilePath, sampleCommentContent) - - describe "when a file doesn't exist", -> - it "calls back with an error", -> - errors = [] - missingPath = path.resolve('/not-a-file.js') - expect(fs.existsSync(missingPath)).toBeFalsy() - - waitsForPromise -> - atom.project.replace /items/gi, 'items', [missingPath], (result, error) -> - errors.push(error) - - runs -> - expect(errors).toHaveLength 1 - expect(errors[0].path).toBe missingPath - - describe "when called with unopened files", -> - it "replaces properly", -> - results = [] - waitsForPromise -> - atom.project.replace /items/gi, 'items', [filePath], (result) -> - results.push(result) - - runs -> - expect(results).toHaveLength 1 - expect(results[0].filePath).toBe filePath - expect(results[0].replacements).toBe 6 - - describe "when a buffer is already open", -> - it "replaces properly and saves when not modified", -> - editor = null - results = [] - - waitsForPromise -> - atom.project.open('sample.js').then (o) -> editor = o - - runs -> - expect(editor.isModified()).toBeFalsy() - - waitsForPromise -> - atom.project.replace /items/gi, 'items', [filePath], (result) -> - results.push(result) - - runs -> - expect(results).toHaveLength 1 - expect(results[0].filePath).toBe filePath - expect(results[0].replacements).toBe 6 - - expect(editor.isModified()).toBeFalsy() - - it "does not replace when the path is not specified", -> - editor = null - results = [] - - waitsForPromise -> - atom.project.open('sample-with-comments.js').then (o) -> editor = o - - waitsForPromise -> - atom.project.replace /items/gi, 'items', [commentFilePath], (result) -> - results.push(result) - - runs -> - expect(results).toHaveLength 1 - expect(results[0].filePath).toBe commentFilePath - - it "does NOT save when modified", -> - editor = null - results = [] - - waitsForPromise -> - atom.project.open('sample.js').then (o) -> editor = o - - runs -> - editor.buffer.setTextInRange([[0,0],[0,0]], 'omg') - expect(editor.isModified()).toBeTruthy() - - waitsForPromise -> - atom.project.replace /items/gi, 'okthen', [filePath], (result) -> - results.push(result) - - runs -> - expect(results).toHaveLength 1 - expect(results[0].filePath).toBe filePath - expect(results[0].replacements).toBe 6 - - expect(editor.isModified()).toBeTruthy() - - describe ".scan(options, callback)", -> - describe "when called with a regex", -> - it "calls the callback with all regex results in all files in the project", -> - results = [] - waitsForPromise -> - atom.project.scan /(a)+/, (result) -> - results.push(result) - - runs -> - expect(results).toHaveLength(3) - expect(results[0].filePath).toBe atom.project.resolve('a') - expect(results[0].matches).toHaveLength(3) - expect(results[0].matches[0]).toEqual - matchText: 'aaa' - lineText: 'aaa bbb' - lineTextOffset: 0 - range: [[0, 0], [0, 3]] - - it "works with with escaped literals (like $ and ^)", -> - results = [] - waitsForPromise -> - atom.project.scan /\$\w+/, (result) -> results.push(result) - - runs -> - expect(results.length).toBe 1 - - {filePath, matches} = results[0] - expect(filePath).toBe atom.project.resolve('a') - expect(matches).toHaveLength 1 - expect(matches[0]).toEqual - matchText: '$bill' - lineText: 'dollar$bill' - lineTextOffset: 0 - range: [[2, 6], [2, 11]] - - it "works on evil filenames", -> - platform.generateEvilFiles() - atom.project.setPaths([path.join(__dirname, 'fixtures', 'evil-files')]) - paths = [] - matches = [] - waitsForPromise -> - atom.project.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", -> - results = [] - waitsForPromise -> - atom.project.scan /DOLLAR/i, (result) -> results.push(result) - - runs -> - expect(results).toHaveLength 1 - - describe "when the core.excludeVcsIgnoredPaths config is truthy", -> - [projectPath, ignoredPath] = [] - - beforeEach -> - sourceProjectPath = path.join(__dirname, 'fixtures', 'git', 'working-dir') - projectPath = path.join(temp.mkdirSync("atom")) - - writerStream = fstream.Writer(projectPath) - fstream.Reader(sourceProjectPath).pipe(writerStream) - - waitsFor (done) -> - writerStream.on 'close', done - writerStream.on 'error', done - - runs -> - fs.rename(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 -> - fs.removeSync(projectPath) if fs.existsSync(projectPath) - - it "excludes ignored files", -> - atom.project.setPaths([projectPath]) - atom.config.set('core.excludeVcsIgnoredPaths', true) - resultHandler = jasmine.createSpy("result found") - waitsForPromise -> - atom.project.scan /match/, (results) -> - resultHandler() - - runs -> - expect(resultHandler).not.toHaveBeenCalled() - - it "includes only files when a directory filter is specified", -> - projectPath = path.join(path.join(__dirname, 'fixtures', 'dir')) - atom.project.setPaths([projectPath]) - - filePath = path.join(projectPath, 'a-dir', 'oh-git') - - paths = [] - matches = [] - waitsForPromise -> - atom.project.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 '.'", -> - projectPath = temp.mkdirSync() - filePath = path.join(projectPath, '.text') - fs.writeFileSync(filePath, 'match this') - atom.project.setPaths([projectPath]) - paths = [] - matches = [] - waitsForPromise -> - atom.project.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", -> - projectPath = path.join(__dirname, 'fixtures', 'git', 'working-dir') - ignoredNames = atom.config.get("core.ignoredNames") - ignoredNames.push("a") - atom.config.set("core.ignoredNames", ignoredNames) - - resultHandler = jasmine.createSpy("result found") - waitsForPromise -> - atom.project.scan /dollar/, (results) -> - resultHandler() - - runs -> - expect(resultHandler).not.toHaveBeenCalled() - - it "scans buffer contents if the buffer is modified", -> - editor = null - results = [] - - waitsForPromise -> - atom.project.open('a').then (o) -> - editor = o - editor.setText("Elephant") - - waitsForPromise -> - atom.project.scan /a|Elephant/, (result) -> results.push result - - runs -> - expect(results).toHaveLength 3 - 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", -> - editor = null - results = [] - - waitsForPromise -> - atom.project.open(temp.openSync().path).then (o) -> - editor = o - editor.setText("Elephant") - - waitsForPromise -> - atom.project.scan /Elephant/, (result) -> results.push result - - runs -> - expect(results).toHaveLength 0 - describe ".eachBuffer(callback)", -> beforeEach -> atom.project.bufferForPathSync('a') diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index 453e70c03..b96bc79af 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -2,6 +2,10 @@ path = require 'path' temp = require 'temp' Workspace = require '../src/workspace' {View} = require '../src/space-pen-extensions' +platform = require './spec-helper-platform' +_ = require 'underscore-plus' +fstream = require 'fstream' +fs = require 'fs-plus' describe "Workspace", -> workspace = null @@ -553,3 +557,287 @@ describe "Workspace", -> expect(atom.workspace.panelForItem(item)).toBe panel expect(atom.workspace.panelForItem(itemWithNoPanel)).toBe null + + 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", -> + results = [] + waitsForPromise -> + atom.workspace.scan /(a)+/, (result) -> + results.push(result) + + runs -> + expect(results).toHaveLength(3) + expect(results[0].filePath).toBe atom.project.resolve('a') + expect(results[0].matches).toHaveLength(3) + expect(results[0].matches[0]).toEqual + matchText: 'aaa' + lineText: 'aaa bbb' + lineTextOffset: 0 + range: [[0, 0], [0, 3]] + + it "works with with escaped literals (like $ and ^)", -> + results = [] + waitsForPromise -> + atom.workspace.scan /\$\w+/, (result) -> results.push(result) + + runs -> + expect(results.length).toBe 1 + + {filePath, matches} = results[0] + expect(filePath).toBe atom.project.resolve('a') + expect(matches).toHaveLength 1 + expect(matches[0]).toEqual + matchText: '$bill' + lineText: 'dollar$bill' + lineTextOffset: 0 + range: [[2, 6], [2, 11]] + + it "works on evil filenames", -> + platform.generateEvilFiles() + atom.project.setPaths([path.join(__dirname, 'fixtures', 'evil-files')]) + paths = [] + 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", -> + results = [] + waitsForPromise -> + atom.workspace.scan /DOLLAR/i, (result) -> results.push(result) + + runs -> + expect(results).toHaveLength 1 + + describe "when the core.excludeVcsIgnoredPaths config is truthy", -> + [projectPath, ignoredPath] = [] + + beforeEach -> + sourceProjectPath = path.join(__dirname, 'fixtures', 'git', 'working-dir') + projectPath = path.join(temp.mkdirSync("atom")) + + writerStream = fstream.Writer(projectPath) + fstream.Reader(sourceProjectPath).pipe(writerStream) + + waitsFor (done) -> + writerStream.on 'close', done + writerStream.on 'error', done + + runs -> + fs.rename(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 -> + fs.removeSync(projectPath) if fs.existsSync(projectPath) + + it "excludes ignored files", -> + atom.project.setPaths([projectPath]) + atom.config.set('core.excludeVcsIgnoredPaths', true) + 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", -> + projectPath = path.join(path.join(__dirname, 'fixtures', 'dir')) + atom.project.setPaths([projectPath]) + + filePath = path.join(projectPath, 'a-dir', 'oh-git') + + paths = [] + 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 '.'", -> + projectPath = temp.mkdirSync() + filePath = path.join(projectPath, '.text') + fs.writeFileSync(filePath, 'match this') + atom.project.setPaths([projectPath]) + paths = [] + 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", -> + projectPath = path.join(__dirname, 'fixtures', 'git', 'working-dir') + ignoredNames = atom.config.get("core.ignoredNames") + ignoredNames.push("a") + atom.config.set("core.ignoredNames", ignoredNames) + + 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", -> + editor = null + results = [] + + waitsForPromise -> + atom.project.open('a').then (o) -> + editor = o + editor.setText("Elephant") + + waitsForPromise -> + atom.workspace.scan /a|Elephant/, (result) -> results.push result + + runs -> + expect(results).toHaveLength 3 + 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", -> + editor = null + results = [] + + waitsForPromise -> + atom.project.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 "::replace(regex, replacementText, paths, iterator)", -> + [filePath, commentFilePath, sampleContent, sampleCommentContent] = [] + + beforeEach -> + atom.project.setPaths([atom.project.resolve('../')]) + + filePath = atom.project.resolve('sample.js') + commentFilePath = atom.project.resolve('sample-with-comments.js') + sampleContent = fs.readFileSync(filePath).toString() + sampleCommentContent = fs.readFileSync(commentFilePath).toString() + + afterEach -> + fs.writeFileSync(filePath, sampleContent) + fs.writeFileSync(commentFilePath, sampleCommentContent) + + describe "when a file doesn't exist", -> + it "calls back with an error", -> + errors = [] + missingPath = path.resolve('/not-a-file.js') + expect(fs.existsSync(missingPath)).toBeFalsy() + + waitsForPromise -> + atom.workspace.replace /items/gi, 'items', [missingPath], (result, error) -> + errors.push(error) + + runs -> + expect(errors).toHaveLength 1 + expect(errors[0].path).toBe missingPath + + describe "when called with unopened files", -> + it "replaces properly", -> + results = [] + waitsForPromise -> + atom.workspace.replace /items/gi, 'items', [filePath], (result) -> + results.push(result) + + runs -> + expect(results).toHaveLength 1 + expect(results[0].filePath).toBe filePath + expect(results[0].replacements).toBe 6 + + describe "when a buffer is already open", -> + it "replaces properly and saves when not modified", -> + editor = null + results = [] + + waitsForPromise -> + atom.project.open('sample.js').then (o) -> editor = o + + runs -> + expect(editor.isModified()).toBeFalsy() + + waitsForPromise -> + atom.workspace.replace /items/gi, 'items', [filePath], (result) -> + results.push(result) + + runs -> + expect(results).toHaveLength 1 + expect(results[0].filePath).toBe filePath + expect(results[0].replacements).toBe 6 + + expect(editor.isModified()).toBeFalsy() + + it "does not replace when the path is not specified", -> + editor = null + results = [] + + waitsForPromise -> + atom.project.open('sample-with-comments.js').then (o) -> editor = o + + waitsForPromise -> + atom.workspace.replace /items/gi, 'items', [commentFilePath], (result) -> + results.push(result) + + runs -> + expect(results).toHaveLength 1 + expect(results[0].filePath).toBe commentFilePath + + it "does NOT save when modified", -> + editor = null + results = [] + + waitsForPromise -> + atom.project.open('sample.js').then (o) -> editor = o + + runs -> + editor.buffer.setTextInRange([[0,0],[0,0]], 'omg') + expect(editor.isModified()).toBeTruthy() + + waitsForPromise -> + atom.workspace.replace /items/gi, 'okthen', [filePath], (result) -> + results.push(result) + + runs -> + expect(results).toHaveLength 1 + expect(results[0].filePath).toBe filePath + expect(results[0].replacements).toBe 6 + + expect(editor.isModified()).toBeTruthy() diff --git a/src/project.coffee b/src/project.coffee index 0eba6c7bd..005c23165 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -176,92 +176,13 @@ class Project extends Model Section: Searching and Replacing ### - # Public: Performs a search across all the files in the project. - # - # * `regex` {RegExp} to search with. - # * `options` (optional) {Object} (default: {}) - # * `paths` An {Array} of glob patterns to search within - # * `iterator` {Function} callback on each file found scan: (regex, options={}, iterator) -> - if _.isFunction(options) - iterator = options - options = {} + Grim.deprecate("Use atom.workspace.scan instead of atom.project.scan") + atom.workspace.scan(regex, options, iterator) - deferred = Q.defer() - - searchOptions = - ignoreCase: regex.ignoreCase - inclusions: options.paths - includeHidden: true - excludeVcsIgnores: atom.config.get('core.excludeVcsIgnoredPaths') - exclusions: atom.config.get('core.ignoredNames') - follow: atom.config.get('core.followSymlinks') - - # TODO: need to support all paths in @getPaths() - task = Task.once require.resolve('./scan-handler'), @getPaths()[0], regex.source, searchOptions, -> - deferred.resolve() - - task.on 'scan:result-found', (result) => - iterator(result) unless @isPathModified(result.filePath) - - task.on 'scan:file-error', (error) -> - iterator(null, error) - - if _.isFunction(options.onPathsSearched) - task.on 'scan:paths-searched', (numberOfPathsSearched) -> - options.onPathsSearched(numberOfPathsSearched) - - for buffer in @getBuffers() when buffer.isModified() - filePath = buffer.getPath() - continue unless @contains(filePath) - matches = [] - buffer.scan regex, (match) -> matches.push match - iterator {filePath, matches} if matches.length > 0 - - promise = deferred.promise - promise.cancel = -> - task.terminate() - deferred.resolve('cancelled') - promise - - # Public: Performs a replace across all the specified files in the project. - # - # * `regex` A {RegExp} to search with. - # * `replacementText` Text to replace all matches of regex with - # * `filePaths` List of file path strings to run the replace on. - # * `iterator` A {Function} callback on each file with replacements: - # * `options` {Object} with keys `filePath` and `replacements` replace: (regex, replacementText, filePaths, iterator) -> - deferred = Q.defer() - - openPaths = (buffer.getPath() for buffer in @getBuffers()) - outOfProcessPaths = _.difference(filePaths, openPaths) - - inProcessFinished = !openPaths.length - outOfProcessFinished = !outOfProcessPaths.length - checkFinished = -> - deferred.resolve() if outOfProcessFinished and inProcessFinished - - unless outOfProcessFinished.length - flags = 'g' - flags += 'i' if regex.ignoreCase - - task = Task.once require.resolve('./replace-handler'), outOfProcessPaths, regex.source, flags, replacementText, -> - outOfProcessFinished = true - checkFinished() - - task.on 'replace:path-replaced', iterator - task.on 'replace:file-error', (error) -> iterator(null, error) - - for buffer in @getBuffers() - continue unless buffer.getPath() in filePaths - replacements = buffer.replace(regex, replacementText, iterator) - iterator({filePath: buffer.getPath(), replacements}) if replacements - - inProcessFinished = true - checkFinished() - - deferred.promise + Grim.deprecate("Use atom.workspace.replace instead of atom.project.replace") + atom.workspace.replace(regex, replacementText, filePaths, iterator) ### Section: Private diff --git a/src/workspace.coffee b/src/workspace.coffee index 00a4550bc..aaca0bf02 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -14,6 +14,7 @@ PanelElement = require './panel-element' PanelContainer = require './panel-container' PanelContainerElement = require './panel-container-element' WorkspaceElement = require './workspace-element' +Task = require './task' # Essential: Represents the state of the user interface for the entire window. # An instance of this class is available via the `atom.workspace` global. @@ -780,3 +781,98 @@ class Workspace extends Model addPanel: (location, options) -> options ?= {} @panelContainers[location].addPanel(new Panel(options)) + + ### + Section: Searching and Replacing + ### + + # Public: Performs a search across all the files in the workspace. + # + # * `regex` {RegExp} to search with. + # * `options` (optional) {Object} (default: {}) + # * `paths` An {Array} of glob patterns to search within + # * `iterator` {Function} callback on each file found + # + # Returns a `Promise`. + scan: (regex, options={}, iterator) -> + if _.isFunction(options) + iterator = options + options = {} + + deferred = Q.defer() + + searchOptions = + ignoreCase: regex.ignoreCase + inclusions: options.paths + includeHidden: true + excludeVcsIgnores: atom.config.get('core.excludeVcsIgnoredPaths') + exclusions: atom.config.get('core.ignoredNames') + follow: atom.config.get('core.followSymlinks') + + # TODO: need to support all paths in @getPaths() + task = Task.once require.resolve('./scan-handler'), atom.project.getPaths()[0], regex.source, searchOptions, -> + deferred.resolve() + + task.on 'scan:result-found', (result) => + iterator(result) unless atom.project.isPathModified(result.filePath) + + task.on 'scan:file-error', (error) -> + iterator(null, error) + + if _.isFunction(options.onPathsSearched) + task.on 'scan:paths-searched', (numberOfPathsSearched) -> + options.onPathsSearched(numberOfPathsSearched) + + for buffer in atom.project.getBuffers() when buffer.isModified() + filePath = buffer.getPath() + continue unless atom.project.contains(filePath) + matches = [] + buffer.scan regex, (match) -> matches.push match + iterator {filePath, matches} if matches.length > 0 + + promise = deferred.promise + promise.cancel = -> + task.terminate() + deferred.resolve('cancelled') + promise + + # Public: Performs a replace across all the specified files in the project. + # + # * `regex` A {RegExp} to search with. + # * `replacementText` Text to replace all matches of regex with + # * `filePaths` List of file path strings to run the replace on. + # * `iterator` A {Function} callback on each file with replacements: + # * `options` {Object} with keys `filePath` and `replacements` + # + # Returns a `Promise`. + replace: (regex, replacementText, filePaths, iterator) -> + deferred = Q.defer() + + openPaths = (buffer.getPath() for buffer in atom.project.getBuffers()) + outOfProcessPaths = _.difference(filePaths, openPaths) + + inProcessFinished = !openPaths.length + outOfProcessFinished = !outOfProcessPaths.length + checkFinished = -> + deferred.resolve() if outOfProcessFinished and inProcessFinished + + unless outOfProcessFinished.length + flags = 'g' + flags += 'i' if regex.ignoreCase + + task = Task.once require.resolve('./replace-handler'), outOfProcessPaths, regex.source, flags, replacementText, -> + outOfProcessFinished = true + checkFinished() + + task.on 'replace:path-replaced', iterator + task.on 'replace:file-error', (error) -> iterator(null, error) + + for buffer in atom.project.getBuffers() + continue unless buffer.getPath() in filePaths + replacements = buffer.replace(regex, replacementText, iterator) + iterator({filePath: buffer.getPath(), replacements}) if replacements + + inProcessFinished = true + checkFinished() + + deferred.promise