diff --git a/spec/task-spec.coffee b/spec/task-spec.coffee index 947db5567..bddb59d86 100644 --- a/spec/task-spec.coffee +++ b/spec/task-spec.coffee @@ -70,29 +70,3 @@ describe "Task", -> task.terminate() expect(stdout.listeners('data').length).toBe 0 expect(stderr.listeners('data').length).toBe 0 - - describe "::cancel()", -> - it "dispatches 'task:cancelled' when invoked on an active task", -> - task = new Task(require.resolve('./fixtures/task-spec-handler')) - cancelledEventSpy = jasmine.createSpy('eventSpy') - task.on('task:cancelled', cancelledEventSpy) - completedEventSpy = jasmine.createSpy('eventSpy') - task.on('task:completed', completedEventSpy) - - expect(task.cancel()).toBe(true) - expect(cancelledEventSpy).toHaveBeenCalled() - expect(completedEventSpy).not.toHaveBeenCalled() - - it "does not dispatch 'task:cancelled' when invoked on an inactive task", -> - handlerResult = null - task = Task.once require.resolve('./fixtures/task-spec-handler'), (result) -> - handlerResult = result - - waitsFor -> - handlerResult? - - runs -> - cancelledEventSpy = jasmine.createSpy('eventSpy') - task.on('task:cancelled', cancelledEventSpy) - expect(task.cancel()).toBe(false) - expect(cancelledEventSpy).not.toHaveBeenCalled() diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index 4666177c6..4918fc65f 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -938,90 +938,6 @@ describe "Workspace", -> .then -> expect(resultPaths).toEqual([file2]) - describe "when a custom directory searcher is registered", -> - fakeSearch = null - # Function that is invoked once all of the fields on fakeSearch are set. - onFakeSearchCreated = null - - class FakeSearch - constructor: (@options) -> - # Note that hoisting resolve and reject in this way is generally frowned upon. - @promise = new Promise (resolve, reject) => - @hoistedResolve = resolve - @hoistedReject = reject - onFakeSearchCreated?(this) - then: (args...) -> - @promise.then.apply(@promise, args) - cancel: -> - @cancelled = true - @hoistedReject() - - beforeEach -> - fakeSearch = null - onFakeSearchCreated = null - atom.packages.serviceHub.provide('atom.directory-searcher', '0.1.0', { - canSearchDirectory: (directory) -> directory.getPath() is dir1 - search: (directory, regex, options) -> fakeSearch = new FakeSearch(options) - }) - - it "can override the DefaultDirectorySearcher on a per-directory basis", -> - foreignFilePath = 'ssh://foreign-directory:8080/hello.txt' - numPathsSearchedInDir2 = 1 - numPathsToPretendToSearchInCustomDirectorySearcher = 10 - 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() - - resultPaths = [] - 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) - - it "can be cancelled when the object returned by scan() has its cancel() method invoked", -> - thenable = atom.workspace.scan /aaaa/, -> - expect(fakeSearch.cancelled).toBe(undefined) - thenable.cancel() - expect(fakeSearch.cancelled).toBe(true) - - resultOfPromiseSearch = null - 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", -> - cancelableSearch = atom.workspace.scan /aaaa/, -> - fakeSearch.hoistedReject() - - didReject = false - waitsForPromise -> - cancelableSearch.catch -> didReject = true - - runs -> - expect(didReject).toBe(true) - describe "::replace(regex, replacementText, paths, iterator)", -> [filePath, commentFilePath, sampleContent, sampleCommentContent] = [] diff --git a/src/default-directory-searcher.coffee b/src/default-directory-searcher.coffee deleted file mode 100644 index ebe3a35f9..000000000 --- a/src/default-directory-searcher.coffee +++ /dev/null @@ -1,95 +0,0 @@ -Task = require './task' - -# Public: Searches local files for lines matching a specified regex. -# -# Implements thenable so it can be used with `Promise.all()`. -class DirectorySearch - constructor: (rootPaths, regex, options) -> - scanHandlerOptions = - ignoreCase: regex.ignoreCase - inclusions: options.inclusions - includeHidden: options.includeHidden - excludeVcsIgnores: options.excludeVcsIgnores - exclusions: options.exclusions - follow: options.follow - @task = new Task(require.resolve('./scan-handler')) - @task.on 'scan:result-found', options.didMatch - @task.on 'scan:file-error', options.didError - @task.on 'scan:paths-searched', options.didSearchPaths - @promise = new Promise (resolve, reject) => - @task.on('task:cancelled', reject) - @task.start(rootPaths, regex.source, scanHandlerOptions, resolve) - - # Public: Implementation of `then()` to satisfy the *thenable* contract. - # This makes it possible to use a `DirectorySearch` with `Promise.all()`. - # - # Returns `Promise`. - then: (args...) -> - @promise.then.apply(@promise, args) - - # Public: Cancels the search. - cancel: -> - # This will cause @promise to reject. - @task.cancel() - null - - -# Default provider for the `atom.directory-searcher` service. -module.exports = -class DefaultDirectorySearcher - # Public: Determines whether this object supports search for a `Directory`. - # - # * `directory` {Directory} whose search needs might be supported by this object. - # - # Returns a `boolean` indicating whether this object can search this `Directory`. - canSearchDirectory: (directory) -> true - - # Public: Performs a text search for files in the specified `Directory`, 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} (This always seems to be 0?) - # * `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, regex, options) -> - rootPaths = directories.map (directory) -> directory.getPath() - isCancelled = false - directorySearch = new DirectorySearch(rootPaths, regex, options) - promise = new Promise (resolve, reject) -> - directorySearch.then resolve, -> - if isCancelled - resolve() - else - reject() - return { - then: promise.then.bind(promise) - cancel: -> - isCancelled = true - directorySearch.cancel() - } diff --git a/src/task.coffee b/src/task.coffee index 34c943c6a..939b71635 100644 --- a/src/task.coffee +++ b/src/task.coffee @@ -150,7 +150,7 @@ class Task # # No more events are emitted once this method is called. terminate: -> - return false unless @childProcess? + return unless @childProcess? @childProcess.removeAllListeners() @childProcess.stdout.removeAllListeners() @@ -158,10 +158,4 @@ class Task @childProcess.kill() @childProcess = null - true - - cancel: -> - didForcefullyTerminate = @terminate() - if didForcefullyTerminate - @emit('task:cancelled') - didForcefullyTerminate + undefined diff --git a/src/workspace.coffee b/src/workspace.coffee index 6311cdd6d..157d002de 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -7,7 +7,6 @@ Serializable = require 'serializable' {Emitter, Disposable, CompositeDisposable} = require 'event-kit' Grim = require 'grim' fs = require 'fs-plus' -DefaultDirectorySearcher = require './default-directory-searcher' Model = require './model' TextEditor = require './text-editor' PaneContainer = require './pane-container' @@ -47,13 +46,6 @@ class Workspace extends Model @paneContainer ?= new PaneContainer() @paneContainer.onDidDestroyPaneItem(@didDestroyPaneItem) - @directorySearchers = [] - @defaultDirectorySearcher = new DefaultDirectorySearcher() - atom.packages.serviceHub.consume( - 'atom.directory-searcher', - '^0.1.0', - (provider) => @directorySearchers.unshift(provider)) - @panelContainers = top: new PanelContainer({location: 'top'}) left: new PanelContainer({location: 'left'}) @@ -799,65 +791,36 @@ class Workspace extends Model # * `regex` {RegExp} to search with. # * `options` (optional) {Object} (default: {}) # * `paths` An {Array} of glob patterns to search within - # * `onPathsSearched` (optional) {Function} # * `iterator` {Function} callback on each file found # - # Returns a `Promise` with a `cancel()` method that will cancel all - # of the underlying searches that were started as part of this scan. + # Returns a `Promise`. scan: (regex, options={}, iterator) -> if _.isFunction(options) iterator = options options = {} - # Find a searcher for every Directory in the project. Each searcher that is matched - # will be associated with an Array of Directory objects in the Map. - directoriesForSearcher = new Map() - for directory in atom.project.getDirectories() - searcher = @defaultDirectorySearcher - for directorySearcher in @directorySearchers - if directorySearcher.canSearchDirectory(directory) - searcher = directorySearcher - break - directories = directoriesForSearcher.get(searcher) - unless directories - directories = [] - directoriesForSearcher.set(searcher, directories) - directories.push(directory) + 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') + + task = Task.once require.resolve('./scan-handler'), atom.project.getPaths(), 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) - # Define the onPathsSearched callback. if _.isFunction(options.onPathsSearched) - # Maintain a map of directories to the number of search results. When notified of a new count, - # replace the entry in the map and update the total. - onPathsSearchedOption = options.onPathsSearched - totalNumberOfPathsSearched = 0 - numberOfPathsSearchedForSearcher = new Map() - onPathsSearched = (searcher, numberOfPathsSearched) -> - oldValue = numberOfPathsSearchedForSearcher.get(searcher) - if oldValue - totalNumberOfPathsSearched -= oldValue - numberOfPathsSearchedForSearcher.set(searcher, numberOfPathsSearched) - totalNumberOfPathsSearched += numberOfPathsSearched - onPathsSearchedOption(totalNumberOfPathsSearched) - else - onPathsSearched = -> - - # Kick off all of the searches and unify them into one Promise. - allSearches = [] - directoriesForSearcher.forEach (directories, searcher) -> - searchOptions = - inclusions: options.paths or [] - includeHidden: true - excludeVcsIgnores: atom.config.get('core.excludeVcsIgnoredPaths') - exclusions: atom.config.get('core.ignoredNames') - follow: atom.config.get('core.followSymlinks') - didMatch: (result) -> - iterator(result) unless atom.project.isPathModified(result.filePath) - didError: (error) -> - iterator(null, error) - didSearchPaths: (count) -> onPathsSearched(searcher, count) - directorySearcher = searcher.search(directories, regex, searchOptions) - allSearches.push(directorySearcher) - searchPromise = Promise.all(allSearches) + task.on 'scan:paths-searched', (numberOfPathsSearched) -> + options.onPathsSearched(numberOfPathsSearched) for buffer in atom.project.getBuffers() when buffer.isModified() filePath = buffer.getPath() @@ -866,33 +829,11 @@ class Workspace extends Model buffer.scan regex, (match) -> matches.push match iterator {filePath, matches} if matches.length > 0 - # Make sure the Promise that is returned to the client is cancelable. To be consistent - # with the existing behavior, instead of cancel() rejecting the promise, it should - # resolve it with the special value 'cancelled'. At least the built-in find-and-replace - # package relies on this behavior. - isCancelled = false - cancellablePromise = new Promise (resolve, reject) -> - onSuccess = -> - resolve(null) - onFailure = -> - if isCancelled - resolve('cancelled') - else - reject() - searchPromise.then(onSuccess, onFailure) - cancellablePromise.cancel = -> - isCancelled = true - # Note that cancelling all (or actually, any) of the members of allSearches - # will cause searchPromise to reject, which will cause cancellablePromise to resolve - # in the desired way. - promise.cancel() for promise in allSearches - - # Although this method claims to return a `Promise`, the `ResultsPaneView.onSearch()` - # method in the find-and-replace package expects the object returned by this method to have a - # `done()` method. Include a done() method until find-and-replace can be updated. - cancellablePromise.done = (onSuccessOrFailure) -> - cancellablePromise.then(onSuccessOrFailure, onSuccessOrFailure) - cancellablePromise + promise = deferred.promise + promise.cancel = -> + task.terminate() + deferred.resolve('cancelled') + promise # Public: Performs a replace across all the specified files in the project. #