diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index b05543ad1..d31164ae3 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", -> @@ -295,190 +294,6 @@ describe "Project", -> 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..e16f756cc 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,187 @@ describe "Workspace", -> expect(atom.workspace.panelForItem(item)).toBe panel expect(atom.workspace.panelForItem(itemWithNoPanel)).toBe null + + 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.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 diff --git a/src/project.coffee b/src/project.coffee index 0eba6c7bd..d8483b844 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -176,53 +176,9 @@ 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 = {} - - 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 + Grim.deprecate("Use atom.workspace.scan instead of atom.project.scan") + atom.workspace.scan(regex, options, iterator) # Public: Performs a replace across all the specified files in the project. # diff --git a/src/workspace.coffee b/src/workspace.coffee index 00a4550bc..8761310dc 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,57 @@ 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