diff --git a/package.json b/package.json index c331fb413..2f677f63e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "0.208.0", + "version": "0.209.0", "description": "A hackable text editor for the 21st Century.", "main": "./src/browser/main.js", "repository": { @@ -32,7 +32,7 @@ "delegato": "^1", "emissary": "^1.3.3", "event-kit": "^1.2.0", - "first-mate": "^4.1.6", + "first-mate": "^4.1.7", "fs-plus": "^2.8.0", "fstream": "0.1.24", "fuzzaldrin": "^2.1", @@ -57,14 +57,14 @@ "scandal": "2.0.3", "scoped-property-store": "^0.17.0", "scrollbar-style": "^3.1", - "season": "^5.1.4", + "season": "^5.3", "semver": "^4.3.3", "serializable": "^1", "service-hub": "^0.5.0", "space-pen": "3.8.2", "stacktrace-parser": "0.1.1", "temp": "0.8.1", - "text-buffer": "6.3.2", + "text-buffer": "6.3.6", "theorist": "^1.0.2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", @@ -120,17 +120,17 @@ "status-bar": "0.74.0", "styleguide": "0.44.0", "symbols-view": "0.98.0", - "tabs": "0.76.0", + "tabs": "0.80.0", "timecop": "0.31.0", "tree-view": "0.172.0", "update-package-dependencies": "0.10.0", - "welcome": "0.28.0", + "welcome": "0.29.0", "whitespace": "0.30.0", "wrap-guide": "0.35.0", "language-c": "0.45.0", "language-clojure": "0.15.0", "language-coffee-script": "0.41.0", - "language-csharp": "0.5.0", + "language-csharp": "0.6.0", "language-css": "0.30.0", "language-gfm": "0.77.0", "language-git": "0.10.0", diff --git a/resources/win/apm.sh b/resources/win/apm.sh index c895f2bce..b50a70a82 100644 --- a/resources/win/apm.sh +++ b/resources/win/apm.sh @@ -1,3 +1,4 @@ #!/bin/sh -"$0/../../app/apm/bin/node.exe" "$0/../../app/apm/lib/cli.js" "$@" +directory=$(dirname "$0") +"$directory/../app/apm/bin/node.exe" "$directory/../app/apm/lib/cli.js" "$@" diff --git a/resources/win/atom.sh b/resources/win/atom.sh index 96370bee5..b6edeeb57 100644 --- a/resources/win/atom.sh +++ b/resources/win/atom.sh @@ -15,8 +15,10 @@ while getopts ":fhtvw-:" opt; do esac done +directory=$(dirname "$0") + if [ $EXPECT_OUTPUT ]; then - "$0/../../../atom.exe" "$@" + "$directory/../../atom.exe" "$@" else - "$0/../../app/apm/bin/node.exe" "$0/../atom.js" "$@" + "$directory/../app/apm/bin/node.exe" "$directory/atom.js" "$@" fi diff --git a/spec/fixtures/packages/package-with-different-directory-name/package.json b/spec/fixtures/packages/package-with-different-directory-name/package.json new file mode 100644 index 000000000..079d0dfb2 --- /dev/null +++ b/spec/fixtures/packages/package-with-different-directory-name/package.json @@ -0,0 +1,4 @@ +{ + "name": "package-with-a-totally-different-name", + "version": "1.0.0" +} diff --git a/spec/fixtures/packages/package-with-invalid-activation-commands/package.json b/spec/fixtures/packages/package-with-invalid-activation-commands/package.json index d98b7ef92..aa3398f59 100644 --- a/spec/fixtures/packages/package-with-invalid-activation-commands/package.json +++ b/spec/fixtures/packages/package-with-invalid-activation-commands/package.json @@ -1,5 +1,5 @@ { - "name": "package-with-invalid-selectors", + "name": "package-with-invalid-activation-commands", "version": "1.0.0", "activationCommands": { "<>": [ diff --git a/spec/fixtures/packages/package-with-main/package.cson b/spec/fixtures/packages/package-with-main/package.cson index a93a109c4..e799f6ca8 100644 --- a/spec/fixtures/packages/package-with-main/package.cson +++ b/spec/fixtures/packages/package-with-main/package.cson @@ -1 +1 @@ -'main': 'main-module.coffee' \ No newline at end of file +'main': 'main-module.coffee' diff --git a/spec/package-spec.coffee b/spec/package-spec.coffee index 87d243ce2..9ea000ab9 100644 --- a/spec/package-spec.coffee +++ b/spec/package-spec.coffee @@ -105,3 +105,13 @@ describe "Package", -> theme.onDidDeactivate spy = jasmine.createSpy() theme.deactivate() expect(spy).toHaveBeenCalled() + + describe ".loadMetadata()", -> + [packagePath, pack, metadata] = [] + + beforeEach -> + packagePath = atom.project.getDirectories()[0]?.resolve('packages/package-with-different-directory-name') + metadata = Package.loadMetadata(packagePath, true) + + it "uses the package name defined in package.json", -> + expect(metadata.name).toBe 'package-with-a-totally-different-name' diff --git a/spec/pane-container-element-spec.coffee b/spec/pane-container-element-spec.coffee index 400904f5c..8947a6d31 100644 --- a/spec/pane-container-element-spec.coffee +++ b/spec/pane-container-element-spec.coffee @@ -154,3 +154,9 @@ describe "PaneContainerElement", -> container.destroy() expect(element.resizeStopped.callCount).toBe 1 expect(document.removeEventListener.callCount).toBe 2 + + it "does not throw an error when resized to fit content in a detached state", -> + container.getActivePane().splitRight() + element = getResizeElement(0) + element.remove() + expect(-> element.resizeToFitContent()).not.toThrow() diff --git a/spec/task-spec.coffee b/spec/task-spec.coffee index bddb59d86..947db5567 100644 --- a/spec/task-spec.coffee +++ b/spec/task-spec.coffee @@ -70,3 +70,29 @@ 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/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 25fde69c1..a0ce19b96 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -1327,6 +1327,10 @@ describe "TextEditorPresenter", -> marker8 = editor.markBufferRange([[2, 2], [2, 2]]) highlight8 = editor.decorateMarker(marker8, type: 'highlight', class: 'h') + # partially off-screen above, empty + marker9 = editor.markBufferRange([[0, 0], [2, 0]], invalidate: 'touch') + highlight9 = editor.decorateMarker(marker9, type: 'highlight', class: 'h') + presenter = buildPresenter(explicitHeight: 30, scrollTop: 20, tileSize: 2) expectUndefinedStateForHighlight(presenter, highlight1) @@ -1388,6 +1392,7 @@ describe "TextEditorPresenter", -> expectUndefinedStateForHighlight(presenter, highlight7) expectUndefinedStateForHighlight(presenter, highlight8) + expectUndefinedStateForHighlight(presenter, highlight9) it "is empty until all of the required measurements are assigned", -> editor.setSelectedBufferRanges([ diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index 4918fc65f..636755346 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -938,6 +938,92 @@ 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 + # According to the spec for a DirectorySearcher, invoking `cancel()` should + # resolve the thenable rather than reject it. + @hoistedResolve() + + 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/atom.coffee b/src/atom.coffee index 0d029b61d..47014ff98 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -472,9 +472,13 @@ class Atom extends Model ipc.send('call-window-method', 'restart') # Extended: Returns a {Boolean} true when the current window is maximized. - isMaximixed: -> + isMaximized: -> @getCurrentWindow().isMaximized() + isMaximixed: -> + deprecate "Use atom.isMaximized() instead" + @isMaximized() + maximize: -> ipc.send('call-window-method', 'maximize') @@ -501,9 +505,9 @@ class Atom extends Model displayWindow: -> dimensions = @restoreWindowDimensions() @show() + @focus() setImmediate => - @focus() @setFullScreen(true) if @workspace?.fullScreen @maximize() if dimensions?.maximized and process.platform isnt 'darwin' diff --git a/src/browser/atom-window.coffee b/src/browser/atom-window.coffee index 9b7c35d03..eaa58ffa5 100644 --- a/src/browser/atom-window.coffee +++ b/src/browser/atom-window.coffee @@ -87,7 +87,7 @@ class AtomWindow hash: encodeURIComponent(JSON.stringify(loadSettings)) getLoadSettings: -> - if @browserWindow.webContents.loaded + if @browserWindow.webContents?.loaded hash = url.parse(@browserWindow.webContents.getUrl()).hash.substr(1) JSON.parse(decodeURIComponent(hash)) @@ -166,7 +166,6 @@ class AtomWindow openLocations: (locationsToOpen) -> if @loaded - @focus() @sendMessage 'open-locations', locationsToOpen else @browserWindow.once 'window:loaded', => @openLocations(locationsToOpen) diff --git a/src/browser/squirrel-update.coffee b/src/browser/squirrel-update.coffee index 1603a7c0a..be90e6cd6 100644 --- a/src/browser/squirrel-update.coffee +++ b/src/browser/squirrel-update.coffee @@ -139,7 +139,7 @@ addCommandsToPath = (callback) -> atomShCommandPath = path.join(binFolder, 'atom') relativeAtomShPath = path.relative(binFolder, path.join(appFolder, 'resources', 'cli', 'atom.sh')) - atomShCommand = "#!/bin/sh\r\n\"$0/../#{relativeAtomShPath.replace(/\\/g, '/')}\" \"$@\"" + atomShCommand = "#!/bin/sh\r\n\"$(dirname \"$0\")/#{relativeAtomShPath.replace(/\\/g, '/')}\" \"$@\"" apmCommandPath = path.join(binFolder, 'apm.cmd') relativeApmPath = path.relative(binFolder, path.join(process.resourcesPath, 'app', 'apm', 'bin', 'apm.cmd')) @@ -147,7 +147,7 @@ addCommandsToPath = (callback) -> apmShCommandPath = path.join(binFolder, 'apm') relativeApmShPath = path.relative(binFolder, path.join(appFolder, 'resources', 'cli', 'apm.sh')) - apmShCommand = "#!/bin/sh\r\n\"$0/../#{relativeApmShPath.replace(/\\/g, '/')}\" \"$@\"" + apmShCommand = "#!/bin/sh\r\n\"$(dirname \"$0\")/#{relativeApmShPath.replace(/\\/g, '/')}\" \"$@\"" fs.writeFile atomCommandPath, atomCommand, -> fs.writeFile atomShCommandPath, atomShCommand, -> diff --git a/src/default-directory-searcher.coffee b/src/default-directory-searcher.coffee new file mode 100644 index 000000000..ebe3a35f9 --- /dev/null +++ b/src/default-directory-searcher.coffee @@ -0,0 +1,95 @@ +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/package.coffee b/src/package.coffee index b404f99e8..3de79ff51 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -45,7 +45,8 @@ class Package throw error unless ignoreErrors metadata ?= {} - metadata.name = packageName + unless typeof metadata.name is 'string' and metadata.name.length > 0 + metadata.name = packageName if includeDeprecatedAPIs and metadata.stylesheetMain? deprecate("Use the `mainStyleSheet` key instead of `stylesheetMain` in the `package.json` of `#{packageName}`", {packageName}) diff --git a/src/pane-resize-handle-element.coffee b/src/pane-resize-handle-element.coffee index 078bb44ac..836dead52 100644 --- a/src/pane-resize-handle-element.coffee +++ b/src/pane-resize-handle-element.coffee @@ -17,8 +17,8 @@ class PaneResizeHandleElement extends HTMLElement resizeToFitContent: -> # clear flex-grow css style of both pane - @previousSibling.model.setFlexScale(1) - @nextSibling.model.setFlexScale(1) + @previousSibling?.model.setFlexScale(1) + @nextSibling?.model.setFlexScale(1) resizeStarted: (e) -> e.stopPropagation() diff --git a/src/selection.coffee b/src/selection.coffee index 6ec874203..b96139e98 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -84,7 +84,7 @@ class Selection extends Model # Public: Modifies the buffer {Range} for the selection. # - # * `screenRange` The new {Range} to select. + # * `bufferRange` The new {Range} to select. # * `options` (optional) {Object} with the keys: # * `preserveFolds` if `true`, the fold settings are preserved after the # selection moves. diff --git a/src/task.coffee b/src/task.coffee index 939b71635..34c943c6a 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 unless @childProcess? + return false unless @childProcess? @childProcess.removeAllListeners() @childProcess.stdout.removeAllListeners() @@ -158,4 +158,10 @@ class Task @childProcess.kill() @childProcess = null - undefined + true + + cancel: -> + didForcefullyTerminate = @terminate() + if didForcefullyTerminate + @emit('task:cancelled') + didForcefullyTerminate diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index b156d236a..6a4612ffa 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1255,13 +1255,6 @@ class TextEditorPresenter range = marker.getScreenRange() if decoration.isDestroyed() or not marker.isValid() or range.isEmpty() or not range.intersectsRowRange(@startRow, @endRow - 1) - tileStartRow = @tileForRow(range.start.row) - tileEndRow = @tileForRow(range.end.row) - - for tile in [tileStartRow..tileEndRow] by @tileSize - delete @state.content.tiles[tile]?.highlights[decoration.id] - - @emitDidUpdateState() return if range.start.row < @startRow @@ -1271,11 +1264,7 @@ class TextEditorPresenter range.end.row = @endRow range.end.column = 0 - if range.isEmpty() - tileState = @state.content.tiles[@tileForRow(range.start.row)] - delete tileState.highlights[decoration.id] - @emitDidUpdateState() - return + return if range.isEmpty() flash = decoration.consumeNextFlash() @@ -1309,8 +1298,6 @@ class TextEditorPresenter @visibleHighlights[tileStartRow] ?= {} @visibleHighlights[tileStartRow][decoration.id] = true - @emitDidUpdateState() - true repositionRegionWithinTile: (region, tileStartRow) -> diff --git a/src/workspace.coffee b/src/workspace.coffee index 157d002de..afc197792 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -7,6 +7,7 @@ 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' @@ -46,6 +47,13 @@ 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'}) @@ -791,36 +799,65 @@ 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`. + # Returns a `Promise` with a `cancel()` method that will cancel all + # of the underlying searches that were started as part of this scan. 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') - - 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) + # 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) + # Define the onPathsSearched callback. if _.isFunction(options.onPathsSearched) - task.on 'scan:paths-searched', (numberOfPathsSearched) -> - options.onPathsSearched(numberOfPathsSearched) + # 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) for buffer in atom.project.getBuffers() when buffer.isModified() filePath = buffer.getPath() @@ -829,11 +866,31 @@ class Workspace extends Model 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 + # 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 = -> + if isCancelled + resolve('cancelled') + else + resolve(null) + searchPromise.then(onSuccess, reject) + cancellablePromise.cancel = -> + isCancelled = true + # Note that cancelling all of the members of allSearches will cause all of the searches + # to resolve, which causes searchPromise to resolve, which is ultimately what causes + # cancellablePromise to resolve. + 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 # Public: Performs a replace across all the specified files in the project. # diff --git a/static/panes.less b/static/panes.less index 163721fcc..1d6d268d8 100644 --- a/static/panes.less +++ b/static/panes.less @@ -25,7 +25,7 @@ atom-pane-container { width: 100%; height: 8px; margin-top: -4px; - cursor: ns-resize; + cursor: row-resize; border-bottom: none; } } @@ -37,7 +37,7 @@ atom-pane-container { width: 8px; height: 100%; margin-left: -4px; - cursor: ew-resize; + cursor: col-resize; border-right: none; } }