From c732ae87740cdb720a62f1e9243854400c69c4a5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 12 Feb 2015 20:37:35 -0800 Subject: [PATCH 01/14] Add atom.pickFolder For showing the native file-picker dialog. The tree-view will use this to add new root folders Signed-off-by: Jessica Lord --- src/atom.coffee | 11 +++++++++++ src/browser/atom-application.coffee | 26 ++++++++++++++++---------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/atom.coffee b/src/atom.coffee index 0036ba420..957acb347 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -407,6 +407,17 @@ class Atom extends Model open: (options) -> ipc.send('open', options) + # Extended: Show the native dialog to prompt the user to select a folder. + # + # * `callback` A {Function} to call once the user has selected a folder. + # * `path` {String} the path to the folder the user selected. + pickFolder: (callback) -> + responseChannel = "atom-pick-folder-response" + ipc.on responseChannel, (path) -> + ipc.removeAllListeners(responseChannel) + callback(path) + ipc.send("pick-folder", responseChannel) + # Essential: Close the current window. close: -> @getCurrentWindow().close() diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index f83e97aa0..6c649d31b 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -152,11 +152,11 @@ class AtomApplication @on 'application:quit', -> app.quit() @on 'application:new-window', -> @openPath(_.extend(windowDimensions: @focusedWindow()?.getDimensions(), getLoadSettings())) @on 'application:new-file', -> (@focusedWindow() ? this).openPath() - @on 'application:open', -> @promptForPath(_.extend(type: 'all', getLoadSettings())) - @on 'application:open-file', -> @promptForPath(_.extend(type: 'file', getLoadSettings())) - @on 'application:open-folder', -> @promptForPath(_.extend(type: 'folder', getLoadSettings())) - @on 'application:open-dev', -> @promptForPath(devMode: true) - @on 'application:open-safe', -> @promptForPath(safeMode: true) + @on 'application:open', -> @promptForPathToOpen('all', getLoadSettings()) + @on 'application:open-file', -> @promptForPathToOpen('file', getLoadSettings()) + @on 'application:open-folder', -> @promptForPathToOpen('folder', getLoadSettings()) + @on 'application:open-dev', -> @promptForPathToOpen('all', devMode: true) + @on 'application:open-safe', -> @promptForPathToOpen('all', safeMode: true) @on 'application:inspect', ({x,y, atomWindow}) -> atomWindow ?= @focusedWindow() atomWindow?.browserWindow.inspectElement(x, y) @@ -227,7 +227,7 @@ class AtomApplication else new AtomWindow(options) else - @promptForPath({window}) + @promptForPathToOpen('all', {window}) ipc.on 'update-application-menu', (event, template, keystrokesByCommand) => win = BrowserWindow.fromWebContents(event.sender) @@ -247,6 +247,10 @@ class AtomApplication win = BrowserWindow.fromWebContents(event.sender) win[method](args...) + ipc.on 'pick-folder', (event, responseChannel) => + @promptForPath "folder", (selectedPaths) -> + event.sender.send(responseChannel, selectedPaths) + clipboard = null ipc.on 'write-text-to-selection-clipboard', (event, selectedText) -> clipboard ?= require 'clipboard' @@ -497,8 +501,11 @@ class AtomApplication # :safeMode - A Boolean which controls whether any newly opened windows # should be in safe mode or not. # :window - An {AtomWindow} to use for opening a selected file path. - promptForPath: ({type, devMode, safeMode, window}={}) -> - type ?= 'all' + promptForPathToOpen: (type, {devMode, safeMode, window}) -> + @promptForPath type, (pathsToOpen) => + @openPaths({pathsToOpen, devMode, safeMode, window}) + + promptForPath: (type, callback) -> properties = switch type when 'file' then ['openFile'] @@ -523,5 +530,4 @@ class AtomApplication openOptions.defaultPath = projectPath dialog = require 'dialog' - dialog.showOpenDialog parentWindow, openOptions, (pathsToOpen) => - @openPaths({pathsToOpen, devMode, safeMode, window}) + dialog.showOpenDialog(parentWindow, openOptions, callback) From ce02c74b76c1198d43feebbe559d08f2935f38c9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 12 Feb 2015 20:37:40 -0800 Subject: [PATCH 02/14] Remove unused instance variable Signed-off-by: Jessica Lord --- src/project.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/project.coffee b/src/project.coffee index b97293310..282a58805 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -165,7 +165,6 @@ class Project extends Model setPaths: (projectPaths) -> [projectPath] = projectPaths projectPath = path.normalize(projectPath) if projectPath - @path = projectPath @rootDirectory?.off() @destroyRepo() From 8ab4ad54d8a535bc72adcf20f39b0810263b03a1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 12 Feb 2015 21:16:15 -0800 Subject: [PATCH 03/14] Allow Project::setPaths to handle multiple paths Signed-off-by: Jessica Lord --- package.json | 1 + spec/project-spec.coffee | 54 +++++++++++++++++++++++++++++++++---- src/project.coffee | 58 +++++++++++++++++++++++----------------- 3 files changed, 84 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 89477634a..258e4cda2 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "less-cache": "0.21", "marked": "^0.3", "mixto": "^1", + "ncp": "^1.0.1", "nslog": "^2.0.0", "oniguruma": "^4.0.0", "optimist": "0.4.0", diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 0a5e06b59..3a9786242 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -7,6 +7,8 @@ path = require 'path' BufferedProcess = require '../src/buffered-process' {Directory} = require 'pathwatcher' GitRepository = require '../src/git-repository' +temp = require "temp" +{ncp} = require "ncp" describe "Project", -> beforeEach -> @@ -228,11 +230,34 @@ describe "Project", -> expect(atom.project.getDirectories()[0].path).toEqual path.dirname(require.resolve('./fixtures/dir/a')) describe "when path is a directory", -> - it "sets its path to the directory and updates the root directory", -> - directory = fs.absolute(path.join(__dirname, 'fixtures', 'dir', 'a-dir')) - atom.project.setPaths([directory]) - expect(atom.project.getPaths()[0]).toEqual directory - expect(atom.project.getDirectories()[0].path).toEqual directory + it "assigns the directories and repositories", -> + directory1 = temp.mkdirSync("non-git-repo") + directory2 = temp.mkdirSync("git-repo1") + directory3 = temp.mkdirSync("git-repo2") + + waitsFor (done) -> + ncp( + fs.absolute(path.join(__dirname, 'fixtures', 'git', 'master.git')), + path.join(directory2, ".git"), + done + ) + + waitsFor (done) -> + ncp( + fs.absolute(path.join(__dirname, 'fixtures', 'git', 'master.git')), + path.join(directory3, ".git"), + done + ) + + runs -> + atom.project.setPaths([directory1, directory2, directory3]) + + [repo1, repo2, repo3] = atom.project.getRepositories() + expect(repo1).toBeNull() + expect(repo2.getShortHead()).toBe "master" + expect(repo2.getPath()).toBe fs.realpathSync(path.join(directory2, ".git")) + expect(repo3.getShortHead()).toBe "master" + expect(repo3.getPath()).toBe fs.realpathSync(path.join(directory3, ".git")) describe "when path is null", -> it "sets its path and root directory to null", -> @@ -245,6 +270,25 @@ 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 ".relativize(path)", -> + it "returns the path, relative to whichever root directory it is inside of", -> + rootPath = atom.project.getPaths()[0] + childPath = path.join(rootPath, "some", "child", "directory") + expect(atom.project.relativize(childPath)).toBe path.join("some", "child", "directory") + + it "returns the given path if it is not in any of the root directories", -> + randomPath = path.join("some", "random", "path") + expect(atom.project.relativize(randomPath)).toBe randomPath + + describe ".contains(path)", -> + it "returns whether or not the given path is in one of the root directories", -> + rootPath = atom.project.getPaths()[0] + childPath = path.join(rootPath, "some", "child", "directory") + expect(atom.project.contains(childPath)).toBe true + + randomPath = path.join("some", "random", "path") + expect(atom.project.contains(randomPath)).toBe false + describe ".eachBuffer(callback)", -> beforeEach -> atom.project.bufferForPathSync('a') diff --git a/src/project.coffee b/src/project.coffee index 282a58805..fed1d7bd3 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -38,6 +38,8 @@ class Project extends Model constructor: ({path, paths, @buffers}={}) -> @emitter = new Emitter @buffers ?= [] + @rootDirectories = [] + @repositories = [] # Mapping from the real path of a {Directory} to a {Promise} that resolves # to either a {Repository} or null. Ideally, the {Directory} would be used @@ -61,12 +63,7 @@ class Project extends Model destroyed: -> buffer.destroy() for buffer in @getBuffers() - @destroyRepo() - - destroyRepo: -> - if @repo? - @repo.destroy() - @repo = null + @setPaths([]) destroyUnretainedBuffers: -> buffer.destroy() for buffer in @getBuffers() when not buffer.isRetained() @@ -118,10 +115,10 @@ class Project extends Model # Promise.all(project.getDirectories().map( # project.repositoryForDirectory.bind(project))) # ``` - getRepositories: -> _.compact([@repo]) + getRepositories: -> @repositories getRepo: -> Grim.deprecate("Use ::getRepositories instead") - @repo + @getRepositories()[0] # Public: Get the repository for a given directory asynchronously. # @@ -154,42 +151,50 @@ class Project extends Model # Public: Get an {Array} of {String}s containing the paths of the project's # directories. - getPaths: -> _.compact([@rootDirectory?.path]) + getPaths: -> rootDirectory.path for rootDirectory in @rootDirectories getPath: -> Grim.deprecate("Use ::getPaths instead") - @rootDirectory?.path + @getPaths()[0] # Public: Set the paths of the project's directories. # # * `projectPaths` {Array} of {String} paths. setPaths: (projectPaths) -> - [projectPath] = projectPaths - projectPath = path.normalize(projectPath) if projectPath - @rootDirectory?.off() + rootDirectory.off() for rootDirectory in @rootDirectories + repository?.destroy() for repository in @repositories + @rootDirectories = [] + @repositories = [] - @destroyRepo() - if projectPath? - directory = if fs.isDirectorySync(projectPath) then projectPath else path.dirname(projectPath) - @rootDirectory = new Directory(directory) + for projectPath, i in projectPaths + projectPath = path.normalize(projectPath) + + directoryPath = if fs.isDirectorySync(projectPath) + projectPath + else + path.dirname(projectPath) + + directory = new Directory(directoryPath) + @rootDirectories.push(directory) # For now, use only the repositoryProviders with a sync API. + repo = null for provider in @repositoryProviders - break if @repo = provider.repositoryForDirectorySync?(@rootDirectory) - else - @rootDirectory = null + break if repo = provider.repositoryForDirectorySync?(directory) + @repositories.push(repo ? null) @emit "path-changed" @emitter.emit 'did-change-paths', projectPaths + setPath: (path) -> Grim.deprecate("Use ::setPaths instead") @setPaths([path]) # Public: Get an {Array} of {Directory}s associated with this project. getDirectories: -> - [@rootDirectory] + @rootDirectories getRootDirectory: -> Grim.deprecate("Use ::getDirectories instead") - @rootDirectory + @getDirectories()[0] resolve: (uri) -> Grim.deprecate("Use `Project::getDirectories()[0]?.resolve()` instead") @@ -203,6 +208,8 @@ class Project extends Model else if fs.isAbsolute(uri) path.normalize(fs.absolute(uri)) + + # TODO: what should we do here when there are multiple directories? else if projectPath = @getPaths()[0] path.normalize(fs.absolute(path.join(projectPath, uri))) else @@ -213,7 +220,10 @@ class Project extends Model # * `fullPath` {String} full path relativize: (fullPath) -> return fullPath if fullPath?.match(/[A-Za-z0-9+-.]+:\/\//) # leave path alone if it has a scheme - @rootDirectory?.relativize(fullPath) ? fullPath + for rootDirectory in @rootDirectories + if (relativePath = rootDirectory.relativize(fullPath))? + return relativePath + fullPath # Public: Determines whether the given path (real or symbolic) is inside the # project's directory. @@ -243,7 +253,7 @@ class Project extends Model # # Returns whether the path is inside the project's root directory. contains: (pathToCheck) -> - @rootDirectory?.contains(pathToCheck) ? false + @rootDirectories.some (dir) -> dir.contains(pathToCheck) ### Section: Searching and Replacing From 4ebfd22e3dfcda1a0c355179911997448e54f15e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 10 Feb 2015 11:53:50 -0800 Subject: [PATCH 04/14] Add Project::addPath --- spec/project-spec.coffee | 23 +++++++++++++++++++++++ src/project.coffee | 39 +++++++++++++++++++++++---------------- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 3a9786242..77aad1a1c 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -259,6 +259,16 @@ describe "Project", -> expect(repo3.getShortHead()).toBe "master" expect(repo3.getPath()).toBe fs.realpathSync(path.join(directory3, ".git")) + it "calls callbacks registered with ::onDidChangePaths", -> + onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') + atom.project.onDidChangePaths(onDidChangePathsSpy) + + paths = [ temp.mkdirSync("dir1"), temp.mkdirSync("dir2") ] + atom.project.setPaths(paths) + + expect(onDidChangePathsSpy.callCount).toBe 1 + expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths) + describe "when path is null", -> it "sets its path and root directory to null", -> atom.project.setPaths([]) @@ -270,6 +280,19 @@ 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 ".addPath(path)", -> + it "calls callbacks registered with ::onDidChangePaths", -> + onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') + atom.project.onDidChangePaths(onDidChangePathsSpy) + + [oldPath] = atom.project.getPaths() + + newPath = temp.mkdirSync("dir") + atom.project.addPath(newPath) + + expect(onDidChangePathsSpy.callCount).toBe 1 + expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual([oldPath, newPath]) + describe ".relativize(path)", -> it "returns the path, relative to whichever root directory it is inside of", -> rootPath = atom.project.getPaths()[0] diff --git a/src/project.coffee b/src/project.coffee index fed1d7bd3..6ce5a6fa2 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -165,22 +165,7 @@ class Project extends Model @rootDirectories = [] @repositories = [] - for projectPath, i in projectPaths - projectPath = path.normalize(projectPath) - - directoryPath = if fs.isDirectorySync(projectPath) - projectPath - else - path.dirname(projectPath) - - directory = new Directory(directoryPath) - @rootDirectories.push(directory) - - # For now, use only the repositoryProviders with a sync API. - repo = null - for provider in @repositoryProviders - break if repo = provider.repositoryForDirectorySync?(directory) - @repositories.push(repo ? null) + @addPath(projectPath, emitEvent: false) for projectPath in projectPaths @emit "path-changed" @emitter.emit 'did-change-paths', projectPaths @@ -189,6 +174,28 @@ class Project extends Model Grim.deprecate("Use ::setPaths instead") @setPaths([path]) + # Public: Add a path the project's list of root paths + # + # * `projectPath` {String} The path to the directory to add. + addPath: (projectPath, options) -> + projectPath = path.normalize(projectPath) + + directoryPath = if fs.isDirectorySync(projectPath) + projectPath + else + path.dirname(projectPath) + directory = new Directory(directoryPath) + @rootDirectories.push(directory) + + repo = null + for provider in @repositoryProviders + break if repo = provider.repositoryForDirectorySync?(directory) + @repositories.push(repo ? null) + + unless options?.emitEvent is false + @emit "path-changed" + @emitter.emit 'did-change-paths', @getPaths() + # Public: Get an {Array} of {Directory}s associated with this project. getDirectories: -> @rootDirectories From ba789800b7a06a67d539fe0f8a704a67546b5bcd Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 12 Feb 2015 21:18:16 -0800 Subject: [PATCH 05/14] Fix handling of args and env in atom-launcher script Signed-off-by: Jessica Lord --- spec/integration/helpers/atom-launcher.sh | 19 ++++++++++++------- spec/integration/helpers/start-atom.coffee | 6 +++--- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/spec/integration/helpers/atom-launcher.sh b/spec/integration/helpers/atom-launcher.sh index 5cc3b8a12..905ed976c 100755 --- a/spec/integration/helpers/atom-launcher.sh +++ b/spec/integration/helpers/atom-launcher.sh @@ -6,10 +6,9 @@ # arguments, so this script accepts the following special switches: # # * `atom-path`: The path to the `Atom` binary. -# * `atom-arg`: A positional argument to pass to Atom. This flag can be specified -# multiple times. -# * `atom-env`: A key=value environment variable to set for Atom. This flag can -# be specified multiple times. +# * `atom-args`: A space-separated list of positional arguments to pass to Atom. +# * `atom-env`: A space-separated list of key=value pairs representing environment +# variables to set for Atom. # # Any other switches will be passed through to `Atom`. @@ -23,12 +22,18 @@ for arg in "$@"; do atom_path="${arg#*=}" ;; - --atom-arg=*) - atom_args+=(${arg#*=}) + --atom-args=*) + atom_arg_string="${arg#*=}" + for atom_arg in $atom_arg_string; do + atom_args+=($atom_arg) + done ;; --atom-env=*) - export ${arg#*=} + atom_env_string="${arg#*=}" + for atom_env_pair in $atom_env_string; do + export $atom_env_pair + done ;; *) diff --git a/spec/integration/helpers/start-atom.coffee b/spec/integration/helpers/start-atom.coffee index 9716b82de..6c5eea295 100644 --- a/spec/integration/helpers/start-atom.coffee +++ b/spec/integration/helpers/start-atom.coffee @@ -50,13 +50,13 @@ module.exports = binary: AtomLauncherPath args: [ "atom-path=#{AtomPath}" + "atom-args=#{args.join(" ")}" + "atom-env=#{map(env, (value, key) -> "#{key}=#{value}").join(" ")}" "dev" "safe" "user-data-dir=#{temp.mkdirSync('integration-spec-')}" "socket-path=#{SocketPath}" - ] - .concat(map args, (arg) -> "atom-arg=#{arg}") - .concat(map env, (value, key) -> "atom-env=#{key}=#{value}")) + ]) .init() .addCommand "waitForCondition", (conditionFn, timeout, cb) -> timedOut = succeeded = false From 81d07e280490c562264a68505640a26246885c0c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 11 Feb 2015 10:53:49 -0800 Subject: [PATCH 06/14] Improve error-handling in integration spec --- spec/integration/helpers/start-atom.coffee | 156 ++++++++++++--------- spec/integration/startup-spec.coffee | 51 +++---- 2 files changed, 114 insertions(+), 93 deletions(-) diff --git a/spec/integration/helpers/start-atom.coffee b/spec/integration/helpers/start-atom.coffee index 6c5eea295..d20d51112 100644 --- a/spec/integration/helpers/start-atom.coffee +++ b/spec/integration/helpers/start-atom.coffee @@ -1,10 +1,10 @@ path = require "path" temp = require("temp").track() remote = require "remote" -{map, extend} = require "underscore-plus" +async = require "async" +{map, extend, once} = require "underscore-plus" {spawn, spawnSync} = require "child_process" webdriverio = require "../../../build/node_modules/webdriverio" -async = require "async" AtomPath = remote.process.argv[0] AtomLauncherPath = path.join(__dirname, "..", "helpers", "atom-launcher.sh") @@ -12,77 +12,107 @@ ChromedriverPath = path.resolve(__dirname, '..', '..', '..', 'atom-shell', 'chro SocketPath = path.join(temp.mkdirSync("socket-dir"), "atom.sock") ChromedriverPort = 9515 -module.exports = - driverTest: (fn) -> - chromedriver = spawn(ChromedriverPath, [ - "--verbose", - "--port=#{ChromedriverPort}", - "--url-base=/wd/hub" - ]) +buildAtomClient = (args, env) -> + client = webdriverio.remote( + host: 'localhost' + port: ChromedriverPort + desiredCapabilities: + browserName: "atom" + chromeOptions: + binary: AtomLauncherPath + args: [ + "atom-path=#{AtomPath}" + "atom-args=#{args.join(" ")}" + "atom-env=#{map(env, (value, key) -> "#{key}=#{value}").join(" ")}" + "dev" + "safe" + "user-data-dir=#{temp.mkdirSync('atom-user-data-dir')}" + "socket-path=#{SocketPath}" + ]) - logs = [] + isRunning = false + client.on "init", -> isRunning = true + client.on "end", -> isRunning = false + + client + .addCommand "waitUntil", (conditionFn, timeout, cb) -> + timedOut = succeeded = false + pollingInterval = Math.min(timeout, 100) + setTimeout((-> timedOut = true), timeout) + async.until( + (-> succeeded or timedOut), + ((next) => + setTimeout(=> + conditionFn.call(this).then( + ((result) -> + succeeded = result + next()), + ((err) -> next(err)) + ) + , pollingInterval)), + ((err) -> cb(err, succeeded))) + + .addCommand "waitForWindowCount", (count, timeout, cb) -> + @waitUntil( + (-> @windowHandles().then(({value}) -> value.length is count)), + timeout) + .then((result) -> expect(result).toBe(true)) + .windowHandles(cb) + + .addCommand "waitForPaneItemCount", (count, timeout, cb) -> + @waitUntil( + (-> @execute((-> atom.workspace.getActivePane().getItems().length)).then ({value}) -> value is count), + timeout) + .then (result) -> + expect(result).toBe(true) + cb(null) + + .addCommand "startAnotherWindow", (args, env, done) -> + @call -> + if isRunning + spawnSync(AtomPath, args.concat([ + "--dev" + "--safe" + "--socket-path=#{SocketPath}" + ]), env: extend({}, process.env, env)) + done() + +module.exports = (args, env, fn) -> + chromedriver = spawn(ChromedriverPath, [ + "--verbose", + "--port=#{ChromedriverPort}", + "--url-base=/wd/hub" + ]) + + chromedriverExit = new Promise (resolve, reject) -> errorCode = null + logs = [] chromedriver.on "exit", (code, signal) -> errorCode = code unless signal? chromedriver.stderr.on "data", (log) -> logs.push(log.toString()) chromedriver.stderr.on "close", -> if errorCode? - jasmine.getEnv().currentSpec.fail "Chromedriver exited. code: #{errorCode}. Logs: #{logs.join("\n")}" + reject("Chromedriver failed. Code: #{errorCode}. Logs: #{logs.join("\n")}") + else + resolve() - waitsFor "webdriver steps to complete", (done) -> - fn() - .catch((error) -> jasmine.getEnv().currentSpec.fail(err.message)) + waitsFor("webdriver to finish", (done) -> + finish = once -> + client .end() - .call(done) - , 30000 + .then(-> chromedriver.kill()) + .then(chromedriverExit.then( + done, + (message) -> + jasmine.getEnv().currentSpec.fail(message) + done())) - runs -> chromedriver.kill() + client = buildAtomClient(args, env) - # Start Atom using chromedriver. - startAtom: (args, env={}) -> - webdriverio.remote( - host: 'localhost' - port: ChromedriverPort - desiredCapabilities: - browserName: "atom" - chromeOptions: - binary: AtomLauncherPath - args: [ - "atom-path=#{AtomPath}" - "atom-args=#{args.join(" ")}" - "atom-env=#{map(env, (value, key) -> "#{key}=#{value}").join(" ")}" - "dev" - "safe" - "user-data-dir=#{temp.mkdirSync('integration-spec-')}" - "socket-path=#{SocketPath}" - ]) - .init() - .addCommand "waitForCondition", (conditionFn, timeout, cb) -> - timedOut = succeeded = false - pollingInterval = Math.min(timeout, 100) + client.on "error", ({err, body}) -> + jasmine.getEnv().currentSpec.fail(err ? body.value.message) + finish() - setTimeout((-> timedOut = true), timeout) - - async.until( - (-> succeeded or timedOut), - ((next) => - setTimeout(=> - conditionFn.call(this).then( - ((result) -> - succeeded = result - next()), - ((err) -> next(err)) - ) - , pollingInterval)), - ((err) -> cb(err, succeeded)) - ) - - # Once one `Atom` window is open, subsequent invocations of `Atom` will exit - # immediately. - startAnotherAtom: (args, env={}) -> - spawnSync(AtomPath, args.concat([ - "--dev" - "--safe" - "--socket-path=#{SocketPath}" - ]), env: extend({}, process.env, env)) + fn(client.init()).then(finish) + , 30000) diff --git a/spec/integration/startup-spec.coffee b/spec/integration/startup-spec.coffee index 4a2a27f6a..0d103005a 100644 --- a/spec/integration/startup-spec.coffee +++ b/spec/integration/startup-spec.coffee @@ -7,34 +7,33 @@ fs = require "fs" path = require "path" temp = require("temp").track() AtomHome = temp.mkdirSync('atom-home') - fs.writeFileSync(path.join(AtomHome, 'config.cson'), fs.readFileSync(path.join(__dirname, 'fixtures', 'atom-home', 'config.cson'))) - -{startAtom, startAnotherAtom, driverTest} = require("./helpers/start-atom") +runAtom = require("./helpers/start-atom") describe "Starting Atom", -> beforeEach -> jasmine.useRealClock() describe "opening paths via commmand-line arguments", -> - [tempDirPath, tempFilePath] = [] + [tempDirPath, tempFilePath, otherTempDirPath] = [] beforeEach -> tempDirPath = temp.mkdirSync("empty-dir") + otherTempDirPath = temp.mkdirSync("another-temp-dir") tempFilePath = path.join(tempDirPath, "an-existing-file") - fs.writeFileSync(tempFilePath, "This was already here.") + fs.writeFileSync(tempFilePath, "This file was already here.") it "reuses existing windows when directories are reopened", -> - driverTest -> + runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: AtomHome}, (client) -> + client - # Opening a new file creates one window with one empty text editor. - startAtom([path.join(tempDirPath, "new-file")], ATOM_HOME: AtomHome) + # Opening a new file creates one window with one empty text editor. .waitForExist("atom-text-editor", 5000) .then((exists) -> expect(exists).toBe true) - .windowHandles() - .then(({value}) -> expect(value.length).toBe 1) - .execute(-> atom.workspace.getActivePane().getItems().length) - .then(({value}) -> expect(value).toBe 1) + .waitForWindowCount(1, 1000) + .waitForPaneItemCount(1, 1000) + .execute(-> atom.project.getPaths()) + .then(({value}) -> expect(value).toEqual([tempDirPath])) # Typing in the editor changes its text. .execute(-> atom.workspace.getActiveTextEditor().getText()) @@ -46,25 +45,17 @@ describe "Starting Atom", -> # Opening an existing file in the same directory reuses the window and # adds a new tab for the file. - .call(-> startAnotherAtom([tempFilePath], ATOM_HOME: AtomHome)) - .waitForCondition( - (-> @execute((-> atom.workspace.getActivePane().getItems().length)).then ({value}) -> value is 2), - 5000) - .then((result) -> expect(result).toBe(true)) + .startAnotherWindow([tempFilePath], ATOM_HOME: AtomHome) + .waitForPaneItemCount(2, 5000) + .waitForWindowCount(1, 1000) .execute(-> atom.workspace.getActiveTextEditor().getText()) - .then(({value}) -> expect(value).toBe "This was already here.") + .then(({value}) -> expect(value).toBe "This file was already here.") # Opening a different directory creates a second window with no # tabs open. - .call(-> startAnotherAtom([temp.mkdirSync("another-empty-dir")], ATOM_HOME: AtomHome)) - .waitForCondition( - (-> @windowHandles().then(({value}) -> value.length is 2)), - 5000) - .then((result) -> expect(result).toBe(true)) - .windowHandles() - .then(({value}) -> - @window(value[1]) - .waitForExist("atom-workspace", 5000) - .then((exists) -> expect(exists).toBe true) - .execute(-> atom.workspace.getActivePane().getItems().length) - .then(({value}) -> expect(value).toBe 0)) + .startAnotherWindow([otherTempDirPath], ATOM_HOME: AtomHome) + .waitForWindowCount(2, 5000) + .then(({value}) -> @window(value[1])) + .waitForExist("atom-workspace", 5000) + .then((exists) -> expect(exists).toBe true) + .waitForPaneItemCount(0, 1000) From f7e1629cfca9d98d36daca719b88ffdf0b979572 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 12 Feb 2015 21:18:18 -0800 Subject: [PATCH 07/14] Set multiple project paths for multiple cmd-line paths Signed-off-by: Jessica Lord --- spec/integration/startup-spec.coffee | 42 ++++++++++++++++++++ spec/project-spec.coffee | 15 +++++++ spec/window-spec.coffee | 34 ++++++++-------- src/atom.coffee | 10 ++--- src/browser/atom-application.coffee | 42 +++++++------------- src/browser/atom-window.coffee | 59 ++++++++++++++++------------ src/project.coffee | 5 +++ src/window-event-handler.coffee | 14 +++---- 8 files changed, 137 insertions(+), 84 deletions(-) diff --git a/spec/integration/startup-spec.coffee b/spec/integration/startup-spec.coffee index 0d103005a..1a2fccdfb 100644 --- a/spec/integration/startup-spec.coffee +++ b/spec/integration/startup-spec.coffee @@ -59,3 +59,45 @@ describe "Starting Atom", -> .waitForExist("atom-workspace", 5000) .then((exists) -> expect(exists).toBe true) .waitForPaneItemCount(0, 1000) + + it "saves the state of closed windows", -> + runAtom [otherTempDirPath], {ATOM_HOME: AtomHome}, (client) -> + client + + # Opening a file in another window creates another window with a tab + # open for that file. + .waitForExist("atom-workspace", 5000) + .startAnotherWindow([tempFilePath], ATOM_HOME: AtomHome) + .waitForWindowCount(2, 5000) + .then(({value}) -> @window(value[1])) + .waitForExist("atom-text-editor", 5000) + .click("atom-text-editor") + .execute(-> atom.workspace.getActiveTextEditor().getText()) + .then(({value}) -> expect(value).toBe "This file was already here.") + + # Closing that window and reopening that directory shows the + # previously-opened file. + .execute(-> atom.unloadEditorWindow()) + .close() + .waitForWindowCount(1, 5000) + .startAnotherWindow([tempDirPath], ATOM_HOME: AtomHome) + .waitForWindowCount(2, 5000) + .then(({value}) -> @window(value[1])) + .waitForExist("atom-text-editor", 5000) + .execute(-> atom.workspace.getActiveTextEditor().getText()) + .then(({value}) -> expect(value).toBe "This file was already here.") + + it "allows multiple project directories to be passed as separate arguments", -> + runAtom [tempDirPath, otherTempDirPath], {ATOM_HOME: AtomHome}, (client) -> + client + .waitForExist("atom-workspace", 5000) + .then((exists) -> expect(exists).toBe true) + .execute(-> atom.project.getPaths()) + .then(({value}) -> expect(value).toEqual([tempDirPath, otherTempDirPath])) + + # Opening a file in one of the directories reuses the same window + # and does not change the project paths. + .startAnotherWindow([tempFilePath], ATOM_HOME: AtomHome) + .waitForPaneItemCount(1, 5000) + .execute(-> atom.project.getPaths()) + .then(({value}) -> expect(value).toEqual([tempDirPath, otherTempDirPath])) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 77aad1a1c..aa27e20e7 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -293,6 +293,21 @@ describe "Project", -> expect(onDidChangePathsSpy.callCount).toBe 1 expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual([oldPath, newPath]) + describe "when the project already has the path or one of its descendants", -> + it "doesn't add it again", -> + onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') + atom.project.onDidChangePaths(onDidChangePathsSpy) + + [oldPath] = atom.project.getPaths() + + atom.project.addPath(oldPath) + atom.project.addPath(path.join(oldPath, "some-file.txt")) + atom.project.addPath(path.join(oldPath, "a-dir")) + atom.project.addPath(path.join(oldPath, "a-dir", "oh-git")) + + expect(atom.project.getPaths()).toEqual([oldPath]) + expect(onDidChangePathsSpy).not.toHaveBeenCalled() + describe ".relativize(path)", -> it "returns the path, relative to whichever root directory it is inside of", -> rootPath = atom.project.getPaths()[0] diff --git a/spec/window-spec.coffee b/spec/window-spec.coffee index 1706e042e..883474e85 100644 --- a/spec/window-spec.coffee +++ b/spec/window-spec.coffee @@ -276,33 +276,31 @@ describe "Window", -> elements.trigger "core:focus-previous" expect(elements.find("[tabindex=1]:focus")).toExist() - describe "the window:open-path event", -> + describe "the window:open-locations event", -> beforeEach -> spyOn(atom.workspace, 'open') + atom.project.setPaths([]) - describe "when the project does not have a path", -> - beforeEach -> - atom.project.setPaths([]) + describe "when the opened path exists", -> + it "adds it to the project's paths", -> + pathToOpen = __filename + atom.getCurrentWindow().send 'message', 'open-locations', [{pathToOpen}] + expect(atom.project.getPaths()[0]).toBe __dirname - describe "when the opened path exists", -> - it "sets the project path to the opened path", -> - atom.getCurrentWindow().send 'message', 'open-path', pathToOpen: __filename - expect(atom.project.getPaths()[0]).toBe __dirname - - describe "when the opened path does not exist but its parent directory does", -> - it "sets the project path to the opened path's parent directory", -> - pathToOpen = path.join(__dirname, 'this-path-does-not-exist.txt') - atom.getCurrentWindow().send 'message', 'open-path', {pathToOpen} - expect(atom.project.getPaths()[0]).toBe __dirname + describe "when the opened path does not exist but its parent directory does", -> + it "adds the parent directory to the project paths", -> + pathToOpen = path.join(__dirname, 'this-path-does-not-exist.txt') + atom.getCurrentWindow().send 'message', 'open-locations', [{pathToOpen}] + expect(atom.project.getPaths()[0]).toBe __dirname describe "when the opened path is a file", -> it "opens it in the workspace", -> - atom.getCurrentWindow().send 'message', 'open-path', pathToOpen: __filename - + pathToOpen = __filename + atom.getCurrentWindow().send 'message', 'open-locations', [{pathToOpen}] expect(atom.workspace.open.mostRecentCall.args[0]).toBe __filename describe "when the opened path is a directory", -> it "does not open it in the workspace", -> - atom.getCurrentWindow().send 'message', 'open-path', pathToOpen: __dirname - + pathToOpen = __dirname + atom.getCurrentWindow().send 'message', 'open-locations', [{pathToOpen}] expect(atom.workspace.open.callCount).toBe 0 diff --git a/src/atom.coffee b/src/atom.coffee index 957acb347..dcc8e0cfb 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -95,9 +95,9 @@ class Atom extends Model when 'spec' filename = 'spec' when 'editor' - {initialPath} = @getLoadSettings() - if initialPath - sha1 = crypto.createHash('sha1').update(initialPath).digest('hex') + {initialPaths} = @getLoadSettings() + if initialPaths + sha1 = crypto.createHash('sha1').update(initialPaths.join("\n")).digest('hex') filename = "editor-#{sha1}" if filename @@ -704,7 +704,7 @@ class Atom extends Model Project = require './project' startTime = Date.now() - @project ?= @deserializers.deserialize(@state.project) ? new Project(paths: [@getLoadSettings().initialPath]) + @project ?= @deserializers.deserialize(@state.project) ? new Project() @deserializeTimings.project = Date.now() - startTime deserializeWorkspaceView: -> @@ -747,7 +747,7 @@ class Atom extends Model # Notify the browser project of the window's current project path watchProjectPath: -> onProjectPathChanged = => - ipc.send('window-command', 'project-path-changed', @project.getPaths()[0]) + ipc.send('window-command', 'project-path-changed', @project.getPaths()) @subscribe @project.onDidChangePaths(onProjectPathChanged) onProjectPathChanged() diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index 6c649d31b..ebb188cc6 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -311,9 +311,9 @@ class AtomApplication else @openPath({pathToOpen}) - # Returns the {AtomWindow} for the given path. - windowForPath: (pathToOpen) -> - _.find @windows, (atomWindow) -> atomWindow.containsPath(pathToOpen) + # Returns the {AtomWindow} for the given paths. + windowForPaths: (pathsToOpen) -> + _.find @windows, (atomWindow) -> atomWindow.containsPaths(pathsToOpen) # Returns the {AtomWindow} for the given ipc event. windowForEvent: ({sender}) -> @@ -327,49 +327,35 @@ class AtomApplication # Public: Opens multiple paths, in existing windows if possible. # # options - - # :pathsToOpen - The array of file paths to open + # :pathToOpen - The file path to open # :pidToKillWhenClosed - The integer of the pid to kill # :newWindow - Boolean of whether this should be opened in a new window. # :devMode - Boolean to control the opened window's dev mode. # :safeMode - Boolean to control the opened window's safe mode. # :window - {AtomWindow} to open file paths in. - openPaths: ({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, window}) -> - for pathToOpen in pathsToOpen ? [] - @openPath({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, window}) + openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, window}) -> + @openPaths({pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, window}) # Public: Opens a single path, in an existing window if possible. # # options - - # :pathToOpen - The file path to open + # :pathsToOpen - The array of file paths to open # :pidToKillWhenClosed - The integer of the pid to kill # :newWindow - Boolean of whether this should be opened in a new window. # :devMode - Boolean to control the opened window's dev mode. # :safeMode - Boolean to control the opened window's safe mode. # :windowDimensions - Object with height and width keys. # :window - {AtomWindow} to open file paths in. - openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, window}={}) -> - {pathToOpen, initialLine, initialColumn} = @locationForPathToOpen(pathToOpen) - pathToOpen = fs.normalize(pathToOpen) + openPaths: ({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, window}={}) -> + pathsToOpen = (fs.normalize(pathToOpen) for pathToOpen in pathsToOpen) + locationsToOpen = (@locationForPathToOpen(pathToOpen) for pathToOpen in pathsToOpen) - unless pidToKillWhenClosed or newWindow - pathToOpenStat = fs.statSyncNoException(pathToOpen) - - # Default to using the specified window or the last focused window - currentWindow = window ? @lastFocusedWindow - - if pathToOpenStat.isFile?() - # Open the file in the current window - existingWindow = currentWindow - else if pathToOpenStat.isDirectory?() - # Open the folder in the current window if it doesn't have a path - existingWindow = currentWindow unless currentWindow?.hasProjectPath() - - # Don't reuse windows in dev mode - existingWindow ?= @windowForPath(pathToOpen) unless devMode + unless pidToKillWhenClosed or newWindow # or devMode + existingWindow = @windowForPaths(pathsToOpen) if existingWindow? openedWindow = existingWindow - openedWindow.openPath(pathToOpen, initialLine, initialColumn) + openedWindow.openLocations(locationsToOpen) if openedWindow.isMinimized() openedWindow.restore() else @@ -382,7 +368,7 @@ class AtomApplication bootstrapScript ?= require.resolve('../window-bootstrap') resourcePath ?= @resourcePath - openedWindow = new AtomWindow({pathToOpen, initialLine, initialColumn, bootstrapScript, resourcePath, devMode, safeMode, windowDimensions}) + openedWindow = new AtomWindow({locationsToOpen, bootstrapScript, resourcePath, devMode, safeMode, windowDimensions}) if pidToKillWhenClosed? @pidsToOpenWindows[pidToKillWhenClosed] = openedWindow diff --git a/src/browser/atom-window.coffee b/src/browser/atom-window.coffee index e2a5e6a0a..bf43b0241 100644 --- a/src/browser/atom-window.coffee +++ b/src/browser/atom-window.coffee @@ -18,7 +18,8 @@ class AtomWindow isSpec: null constructor: (settings={}) -> - {@resourcePath, pathToOpen, initialLine, initialColumn, @isSpec, @exitWhenDone, @safeMode, @devMode} = settings + {@resourcePath, pathToOpen, @locationsToOpen, @isSpec, @exitWhenDone, @safeMode, @devMode} = settings + @locationsToOpen ?= [{pathToOpen}] if pathToOpen # Normalize to make sure drive letter case is consistent on Windows @resourcePath = path.normalize(@resourcePath) if @resourcePath @@ -51,21 +52,24 @@ class AtomWindow @constructor.includeShellLoadTime = false loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime - loadSettings.initialPath = pathToOpen - if fs.statSyncNoException(pathToOpen).isFile?() - loadSettings.initialPath = path.dirname(pathToOpen) + loadSettings.initialPaths = for {pathToOpen} in (@locationsToOpen ? []) + if fs.statSyncNoException(pathToOpen).isFile?() + path.dirname(pathToOpen) + else + pathToOpen + loadSettings.initialPaths.sort() @browserWindow.loadSettings = loadSettings @browserWindow.once 'window:loaded', => @emit 'window:loaded' @loaded = true - @browserWindow.on 'project-path-changed', (@projectPath) => + @browserWindow.on 'project-path-changed', (@projectPaths) => @browserWindow.loadUrl @getUrl(loadSettings) @browserWindow.focusOnWebView() if @isSpec - @openPath(pathToOpen, initialLine, initialColumn) unless @isSpecWindow() + @openLocations(@locationsToOpen) unless @isSpecWindow() getUrl: (loadSettingsObj) -> # Ignore the windowState when passing loadSettings via URL, since it could @@ -79,10 +83,7 @@ class AtomWindow slashes: true query: {loadSettings: JSON.stringify(loadSettings)} - hasProjectPath: -> @projectPath?.length > 0 - - getInitialPath: -> - @browserWindow.loadSettings.initialPath + hasProjectPath: -> @projectPaths?.length > 0 setupContextMenu: -> ContextMenu = null @@ -91,20 +92,25 @@ class AtomWindow ContextMenu ?= require './context-menu' new ContextMenu(menuTemplate, this) + containsPaths: (paths) -> + for pathToCheck in paths + return false unless @containsPath(pathToCheck) + true + containsPath: (pathToCheck) -> - initialPath = @getInitialPath() - if not initialPath - false - else if not pathToCheck - false - else if pathToCheck is initialPath - true - else if fs.statSyncNoException(pathToCheck).isDirectory?() - false - else if pathToCheck.indexOf(path.join(initialPath, path.sep)) is 0 - true - else - false + @projectPaths.some (projectPath) -> + if not projectPath + false + else if not pathToCheck + false + else if pathToCheck is projectPath + true + else if fs.statSyncNoException(pathToCheck).isDirectory?() + false + else if pathToCheck.indexOf(path.join(projectPath, path.sep)) is 0 + true + else + false handleEvents: -> @browserWindow.on 'closed', => @@ -148,11 +154,14 @@ class AtomWindow @browserWindow.focusOnWebView() unless @isWindowClosing openPath: (pathToOpen, initialLine, initialColumn) -> + @openLocations([{pathToOpen, initialLine, initialColumn}]) + + openLocations: (locationsToOpen) -> if @loaded @focus() - @sendMessage 'open-path', {pathToOpen, initialLine, initialColumn} + @sendMessage 'open-locations', locationsToOpen else - @browserWindow.once 'window:loaded', => @openPath(pathToOpen, initialLine, initialColumn) + @browserWindow.once 'window:loaded', => @openLocations(locationsToOpen) sendMessage: (message, detail) -> @browserWindow.webContents.send 'message', message, detail diff --git a/src/project.coffee b/src/project.coffee index 6ce5a6fa2..150e28d9a 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -184,6 +184,11 @@ class Project extends Model projectPath else path.dirname(projectPath) + + return if @getPaths().some (existingPath) -> + (directoryPath is existingPath) or + (directoryPath.indexOf(path.join(existingPath, path.sep)) is 0) + directory = new Directory(directoryPath) @rootDirectories.push(directory) diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index d2a79b2a4..6d68f3437 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -17,15 +17,13 @@ class WindowEventHandler @subscribe ipc, 'message', (message, detail) -> switch message - when 'open-path' - {pathToOpen, initialLine, initialColumn} = detail + when 'open-locations' + for {pathToOpen, initialLine, initialColumn} in detail + if pathToOpen and (fs.existsSync(pathToOpen) or fs.existsSync(path.dirname(pathToOpen))) + atom.project?.addPath(pathToOpen) - unless atom.project?.getPaths().length - if fs.existsSync(pathToOpen) or fs.existsSync(path.dirname(pathToOpen)) - atom.project?.setPaths([pathToOpen]) - - unless fs.isDirectorySync(pathToOpen) - atom.workspace?.open(pathToOpen, {initialLine, initialColumn}) + unless fs.isDirectorySync(pathToOpen) + atom.workspace?.open(pathToOpen, {initialLine, initialColumn}) when 'update-available' atom.updateAvailable(detail) From 29662efe7c5e4c93cad70508069f990cde7ceb3e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 11 Feb 2015 16:19:59 -0800 Subject: [PATCH 08/14] Don't re-use dev windows for non-dev or vise-versa --- src/browser/atom-application.coffee | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index ebb188cc6..e4a2e4784 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -312,8 +312,9 @@ class AtomApplication @openPath({pathToOpen}) # Returns the {AtomWindow} for the given paths. - windowForPaths: (pathsToOpen) -> - _.find @windows, (atomWindow) -> atomWindow.containsPaths(pathsToOpen) + windowForPaths: (pathsToOpen, devMode) -> + _.find @windows, (atomWindow) -> + atomWindow.devMode is devMode and atomWindow.containsPaths(pathsToOpen) # Returns the {AtomWindow} for the given ipc event. windowForEvent: ({sender}) -> @@ -351,7 +352,7 @@ class AtomApplication locationsToOpen = (@locationForPathToOpen(pathToOpen) for pathToOpen in pathsToOpen) unless pidToKillWhenClosed or newWindow # or devMode - existingWindow = @windowForPaths(pathsToOpen) + existingWindow = @windowForPaths(pathsToOpen, devMode) if existingWindow? openedWindow = existingWindow From e0aa8e7f5ccb2cb0cc833129e820ebce0c1c80bc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 11 Feb 2015 16:55:44 -0800 Subject: [PATCH 09/14] Move ncp dependency to build/package.json --- build/package.json | 1 + package.json | 1 - spec/project-spec.coffee | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/package.json b/build/package.json index 4bfc940a2..c2d9fce1b 100644 --- a/build/package.json +++ b/build/package.json @@ -26,6 +26,7 @@ "harmony-collections": "~0.3.8", "legal-eagle": "~0.9.0", "minidump": "~0.8", + "ncp": "^1.0.1", "npm": "~1.4.5", "rcedit": "~0.3.0", "request": "~2.27.0", diff --git a/package.json b/package.json index 258e4cda2..89477634a 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "less-cache": "0.21", "marked": "^0.3", "mixto": "^1", - "ncp": "^1.0.1", "nslog": "^2.0.0", "oniguruma": "^4.0.0", "optimist": "0.4.0", diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index aa27e20e7..1dcb73721 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -8,7 +8,7 @@ BufferedProcess = require '../src/buffered-process' {Directory} = require 'pathwatcher' GitRepository = require '../src/git-repository' temp = require "temp" -{ncp} = require "ncp" +{ncp} = require "../build/node_modules/ncp" describe "Project", -> beforeEach -> From 476876e4799172fc5e0527bc058badd5afb8bdca Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 11 Feb 2015 17:04:54 -0800 Subject: [PATCH 10/14] Restore behavior of reusing focused window for opened files --- spec/integration/helpers/start-atom.coffee | 13 +++++++- spec/integration/startup-spec.coffee | 36 ++++++++++------------ src/browser/atom-application.coffee | 6 +++- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/spec/integration/helpers/start-atom.coffee b/spec/integration/helpers/start-atom.coffee index d20d51112..74ec3af95 100644 --- a/spec/integration/helpers/start-atom.coffee +++ b/spec/integration/helpers/start-atom.coffee @@ -2,7 +2,7 @@ path = require "path" temp = require("temp").track() remote = require "remote" async = require "async" -{map, extend, once} = require "underscore-plus" +{map, extend, once, difference} = require "underscore-plus" {spawn, spawnSync} = require "child_process" webdriverio = require "../../../build/node_modules/webdriverio" @@ -67,6 +67,17 @@ buildAtomClient = (args, env) -> expect(result).toBe(true) cb(null) + .addCommand("waitForNewWindow", (fn, timeout, done) -> + @windowHandles() + .then(({value}) -> + return done() unless isRunning + oldWindowHandles = value + @call(-> fn.call(this)) + .waitForWindowCount(oldWindowHandles.length + 1, 5000) + .then(({value}) -> + [newWindowHandle] = difference(value, oldWindowHandles) + @window(newWindowHandle, done)))) + .addCommand "startAnotherWindow", (args, env, done) -> @call -> if isRunning diff --git a/spec/integration/startup-spec.coffee b/spec/integration/startup-spec.coffee index 1a2fccdfb..004aaff2e 100644 --- a/spec/integration/startup-spec.coffee +++ b/spec/integration/startup-spec.coffee @@ -53,39 +53,35 @@ describe "Starting Atom", -> # Opening a different directory creates a second window with no # tabs open. - .startAnotherWindow([otherTempDirPath], ATOM_HOME: AtomHome) - .waitForWindowCount(2, 5000) - .then(({value}) -> @window(value[1])) + .waitForNewWindow(-> + @startAnotherWindow([otherTempDirPath], ATOM_HOME: AtomHome) + , 5000) .waitForExist("atom-workspace", 5000) .then((exists) -> expect(exists).toBe true) .waitForPaneItemCount(0, 1000) it "saves the state of closed windows", -> - runAtom [otherTempDirPath], {ATOM_HOME: AtomHome}, (client) -> + runAtom [tempDirPath], {ATOM_HOME: AtomHome}, (client) -> client - # Opening a file in another window creates another window with a tab - # open for that file. + # In a second window, opening a new buffer creates a new tab. .waitForExist("atom-workspace", 5000) - .startAnotherWindow([tempFilePath], ATOM_HOME: AtomHome) - .waitForWindowCount(2, 5000) - .then(({value}) -> @window(value[1])) - .waitForExist("atom-text-editor", 5000) - .click("atom-text-editor") - .execute(-> atom.workspace.getActiveTextEditor().getText()) - .then(({value}) -> expect(value).toBe "This file was already here.") + .waitForNewWindow(-> + @startAnotherWindow([otherTempDirPath], ATOM_HOME: AtomHome) + , 5000) + .waitForPaneItemCount(0, 3000) + .execute(-> atom.workspace.open()) + .waitForPaneItemCount(1, 3000) # Closing that window and reopening that directory shows the - # previously-opened file. + # previously-created new buffer. .execute(-> atom.unloadEditorWindow()) .close() .waitForWindowCount(1, 5000) - .startAnotherWindow([tempDirPath], ATOM_HOME: AtomHome) - .waitForWindowCount(2, 5000) - .then(({value}) -> @window(value[1])) - .waitForExist("atom-text-editor", 5000) - .execute(-> atom.workspace.getActiveTextEditor().getText()) - .then(({value}) -> expect(value).toBe "This file was already here.") + .waitForNewWindow(-> + @startAnotherWindow([otherTempDirPath], ATOM_HOME: AtomHome) + , 5000) + .waitForPaneItemCount(1, 5000) it "allows multiple project directories to be passed as separate arguments", -> runAtom [tempDirPath, otherTempDirPath], {ATOM_HOME: AtomHome}, (client) -> diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index e4a2e4784..2f46ba6d2 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -351,9 +351,13 @@ class AtomApplication pathsToOpen = (fs.normalize(pathToOpen) for pathToOpen in pathsToOpen) locationsToOpen = (@locationForPathToOpen(pathToOpen) for pathToOpen in pathsToOpen) - unless pidToKillWhenClosed or newWindow # or devMode + unless pidToKillWhenClosed or newWindow existingWindow = @windowForPaths(pathsToOpen, devMode) + # Default to using the specified window or the last focused window + if pathsToOpen.every((pathToOpen) -> fs.statSyncNoException(pathToOpen).isFile?()) + existingWindow ?= window ? @lastFocusedWindow + if existingWindow? openedWindow = existingWindow openedWindow.openLocations(locationsToOpen) From a5b28b2833f3d5e563df20c9c5d05dd511f91463 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 11 Feb 2015 20:00:58 -0800 Subject: [PATCH 11/14] Rename test helper method --- spec/integration/helpers/start-atom.coffee | 2 +- spec/integration/startup-spec.coffee | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/integration/helpers/start-atom.coffee b/spec/integration/helpers/start-atom.coffee index 74ec3af95..59a6e43f5 100644 --- a/spec/integration/helpers/start-atom.coffee +++ b/spec/integration/helpers/start-atom.coffee @@ -78,7 +78,7 @@ buildAtomClient = (args, env) -> [newWindowHandle] = difference(value, oldWindowHandles) @window(newWindowHandle, done)))) - .addCommand "startAnotherWindow", (args, env, done) -> + .addCommand "startAnotherAtom", (args, env, done) -> @call -> if isRunning spawnSync(AtomPath, args.concat([ diff --git a/spec/integration/startup-spec.coffee b/spec/integration/startup-spec.coffee index 004aaff2e..95995f6dc 100644 --- a/spec/integration/startup-spec.coffee +++ b/spec/integration/startup-spec.coffee @@ -45,7 +45,7 @@ describe "Starting Atom", -> # Opening an existing file in the same directory reuses the window and # adds a new tab for the file. - .startAnotherWindow([tempFilePath], ATOM_HOME: AtomHome) + .startAnotherAtom([tempFilePath], ATOM_HOME: AtomHome) .waitForPaneItemCount(2, 5000) .waitForWindowCount(1, 1000) .execute(-> atom.workspace.getActiveTextEditor().getText()) @@ -54,7 +54,7 @@ describe "Starting Atom", -> # Opening a different directory creates a second window with no # tabs open. .waitForNewWindow(-> - @startAnotherWindow([otherTempDirPath], ATOM_HOME: AtomHome) + @startAnotherAtom([otherTempDirPath], ATOM_HOME: AtomHome) , 5000) .waitForExist("atom-workspace", 5000) .then((exists) -> expect(exists).toBe true) @@ -67,7 +67,7 @@ describe "Starting Atom", -> # In a second window, opening a new buffer creates a new tab. .waitForExist("atom-workspace", 5000) .waitForNewWindow(-> - @startAnotherWindow([otherTempDirPath], ATOM_HOME: AtomHome) + @startAnotherAtom([otherTempDirPath], ATOM_HOME: AtomHome) , 5000) .waitForPaneItemCount(0, 3000) .execute(-> atom.workspace.open()) @@ -79,7 +79,7 @@ describe "Starting Atom", -> .close() .waitForWindowCount(1, 5000) .waitForNewWindow(-> - @startAnotherWindow([otherTempDirPath], ATOM_HOME: AtomHome) + @startAnotherAtom([otherTempDirPath], ATOM_HOME: AtomHome) , 5000) .waitForPaneItemCount(1, 5000) @@ -93,7 +93,7 @@ describe "Starting Atom", -> # Opening a file in one of the directories reuses the same window # and does not change the project paths. - .startAnotherWindow([tempFilePath], ATOM_HOME: AtomHome) + .startAnotherAtom([tempFilePath], ATOM_HOME: AtomHome) .waitForPaneItemCount(1, 5000) .execute(-> atom.project.getPaths()) .then(({value}) -> expect(value).toEqual([tempDirPath, otherTempDirPath])) From 8cc0372432051b5c100a753c63f20efea2270190 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 11 Feb 2015 20:07:31 -0800 Subject: [PATCH 12/14] Avoid unhandled promise rejection warning --- spec/integration/helpers/start-atom.coffee | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/integration/helpers/start-atom.coffee b/spec/integration/helpers/start-atom.coffee index 59a6e43f5..ad40bcb92 100644 --- a/spec/integration/helpers/start-atom.coffee +++ b/spec/integration/helpers/start-atom.coffee @@ -95,7 +95,7 @@ module.exports = (args, env, fn) -> "--url-base=/wd/hub" ]) - chromedriverExit = new Promise (resolve, reject) -> + chromedriverExit = new Promise (resolve) -> errorCode = null logs = [] chromedriver.on "exit", (code, signal) -> @@ -103,10 +103,7 @@ module.exports = (args, env, fn) -> chromedriver.stderr.on "data", (log) -> logs.push(log.toString()) chromedriver.stderr.on "close", -> - if errorCode? - reject("Chromedriver failed. Code: #{errorCode}. Logs: #{logs.join("\n")}") - else - resolve() + resolve({errorCode, logs}) waitsFor("webdriver to finish", (done) -> finish = once -> @@ -114,15 +111,18 @@ module.exports = (args, env, fn) -> .end() .then(-> chromedriver.kill()) .then(chromedriverExit.then( - done, - (message) -> - jasmine.getEnv().currentSpec.fail(message) + ({errorCode, logs}) -> + if errorCode? + jasmine.getEnv().currentSpec.fail """ + Chromedriver exited with code #{errorCode}. + Logs:\n#{logs.join("\n")} + """ done())) client = buildAtomClient(args, env) - client.on "error", ({err, body}) -> - jasmine.getEnv().currentSpec.fail(err ? body.value.message) + client.on "error", ({body}) -> + jasmine.getEnv().currentSpec.fail(body) finish() fn(client.init()).then(finish) From 30bd85d8cc1fc6e065247ec28f106f2bc47223ad Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 11 Feb 2015 20:29:17 -0800 Subject: [PATCH 13/14] Add some waiting in integration test * Give chromedriver a few millis to start up * Wait for atom-workspace element to appear before querying active panes. --- spec/integration/helpers/start-atom.coffee | 22 ++++++++++++---------- spec/integration/startup-spec.coffee | 5 ++++- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/spec/integration/helpers/start-atom.coffee b/spec/integration/helpers/start-atom.coffee index ad40bcb92..8597dabdd 100644 --- a/spec/integration/helpers/start-atom.coffee +++ b/spec/integration/helpers/start-atom.coffee @@ -60,9 +60,9 @@ buildAtomClient = (args, env) -> .windowHandles(cb) .addCommand "waitForPaneItemCount", (count, timeout, cb) -> - @waitUntil( - (-> @execute((-> atom.workspace.getActivePane().getItems().length)).then ({value}) -> value is count), - timeout) + @waitUntil((-> + @execute(-> atom.workspace?.getActivePane()?.getItems().length) + .then(({value}) -> value is count)), timeout) .then (result) -> expect(result).toBe(true) cb(null) @@ -95,15 +95,17 @@ module.exports = (args, env, fn) -> "--url-base=/wd/hub" ]) + waits(50) + + chromedriverLogs = [] chromedriverExit = new Promise (resolve) -> errorCode = null - logs = [] chromedriver.on "exit", (code, signal) -> errorCode = code unless signal? chromedriver.stderr.on "data", (log) -> - logs.push(log.toString()) + chromedriverLogs.push(log.toString()) chromedriver.stderr.on "close", -> - resolve({errorCode, logs}) + resolve(errorCode) waitsFor("webdriver to finish", (done) -> finish = once -> @@ -111,18 +113,18 @@ module.exports = (args, env, fn) -> .end() .then(-> chromedriver.kill()) .then(chromedriverExit.then( - ({errorCode, logs}) -> + (errorCode) -> if errorCode? jasmine.getEnv().currentSpec.fail """ Chromedriver exited with code #{errorCode}. - Logs:\n#{logs.join("\n")} + Logs:\n#{chromedriverLogs.join("\n")} """ done())) client = buildAtomClient(args, env) - client.on "error", ({body}) -> - jasmine.getEnv().currentSpec.fail(body) + client.on "error", (err) -> + jasmine.getEnv().currentSpec.fail(JSON.stringify(err)) finish() fn(client.init()).then(finish) diff --git a/spec/integration/startup-spec.coffee b/spec/integration/startup-spec.coffee index 95995f6dc..4cc5b1525 100644 --- a/spec/integration/startup-spec.coffee +++ b/spec/integration/startup-spec.coffee @@ -46,6 +46,7 @@ describe "Starting Atom", -> # Opening an existing file in the same directory reuses the window and # adds a new tab for the file. .startAnotherAtom([tempFilePath], ATOM_HOME: AtomHome) + .waitForExist("atom-workspace") .waitForPaneItemCount(2, 5000) .waitForWindowCount(1, 1000) .execute(-> atom.workspace.getActiveTextEditor().getText()) @@ -57,7 +58,6 @@ describe "Starting Atom", -> @startAnotherAtom([otherTempDirPath], ATOM_HOME: AtomHome) , 5000) .waitForExist("atom-workspace", 5000) - .then((exists) -> expect(exists).toBe true) .waitForPaneItemCount(0, 1000) it "saves the state of closed windows", -> @@ -69,6 +69,7 @@ describe "Starting Atom", -> .waitForNewWindow(-> @startAnotherAtom([otherTempDirPath], ATOM_HOME: AtomHome) , 5000) + .waitForExist("atom-workspace", 5000) .waitForPaneItemCount(0, 3000) .execute(-> atom.workspace.open()) .waitForPaneItemCount(1, 3000) @@ -81,6 +82,7 @@ describe "Starting Atom", -> .waitForNewWindow(-> @startAnotherAtom([otherTempDirPath], ATOM_HOME: AtomHome) , 5000) + .waitForExist("atom-workspace", 5000) .waitForPaneItemCount(1, 5000) it "allows multiple project directories to be passed as separate arguments", -> @@ -94,6 +96,7 @@ describe "Starting Atom", -> # Opening a file in one of the directories reuses the same window # and does not change the project paths. .startAnotherAtom([tempFilePath], ATOM_HOME: AtomHome) + .waitForExist("atom-workspace", 5000) .waitForPaneItemCount(1, 5000) .execute(-> atom.project.getPaths()) .then(({value}) -> expect(value).toEqual([tempDirPath, otherTempDirPath])) From ac78cee584da578d85a8eb8ff595ef859f9e1b89 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 12 Feb 2015 22:14:02 -0800 Subject: [PATCH 14/14] :fire: ncp; use fs.copySync --- build/package.json | 1 - spec/project-spec.coffee | 32 ++++++++++---------------------- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/build/package.json b/build/package.json index c2d9fce1b..4bfc940a2 100644 --- a/build/package.json +++ b/build/package.json @@ -26,7 +26,6 @@ "harmony-collections": "~0.3.8", "legal-eagle": "~0.9.0", "minidump": "~0.8", - "ncp": "^1.0.1", "npm": "~1.4.5", "rcedit": "~0.3.0", "request": "~2.27.0", diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 1dcb73721..dbd0a0fbd 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -8,7 +8,6 @@ BufferedProcess = require '../src/buffered-process' {Directory} = require 'pathwatcher' GitRepository = require '../src/git-repository' temp = require "temp" -{ncp} = require "../build/node_modules/ncp" describe "Project", -> beforeEach -> @@ -235,29 +234,18 @@ describe "Project", -> directory2 = temp.mkdirSync("git-repo1") directory3 = temp.mkdirSync("git-repo2") - waitsFor (done) -> - ncp( - fs.absolute(path.join(__dirname, 'fixtures', 'git', 'master.git')), - path.join(directory2, ".git"), - done - ) + gitDirPath = fs.absolute(path.join(__dirname, 'fixtures', 'git', 'master.git')) + fs.copySync(gitDirPath, path.join(directory2, ".git")) + fs.copySync(gitDirPath, path.join(directory3, ".git")) - waitsFor (done) -> - ncp( - fs.absolute(path.join(__dirname, 'fixtures', 'git', 'master.git')), - path.join(directory3, ".git"), - done - ) + atom.project.setPaths([directory1, directory2, directory3]) - runs -> - atom.project.setPaths([directory1, directory2, directory3]) - - [repo1, repo2, repo3] = atom.project.getRepositories() - expect(repo1).toBeNull() - expect(repo2.getShortHead()).toBe "master" - expect(repo2.getPath()).toBe fs.realpathSync(path.join(directory2, ".git")) - expect(repo3.getShortHead()).toBe "master" - expect(repo3.getPath()).toBe fs.realpathSync(path.join(directory3, ".git")) + [repo1, repo2, repo3] = atom.project.getRepositories() + expect(repo1).toBeNull() + expect(repo2.getShortHead()).toBe "master" + expect(repo2.getPath()).toBe fs.realpathSync(path.join(directory2, ".git")) + expect(repo3.getShortHead()).toBe "master" + expect(repo3.getPath()).toBe fs.realpathSync(path.join(directory3, ".git")) it "calls callbacks registered with ::onDidChangePaths", -> onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')