Merge pull request #5475 from atom/mb-multi-folder-project

Allow project API to support multiple root directories
This commit is contained in:
Max Brunsfeld 2015-02-12 22:28:16 -08:00
commit 2dac7fd18e
10 changed files with 411 additions and 227 deletions

View File

@ -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
;;
*)

View File

@ -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, difference} = 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,120 @@ 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("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 "startAnotherAtom", (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"
])
waits(50)
chromedriverLogs = []
chromedriverExit = new Promise (resolve) ->
errorCode = null
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", ->
if errorCode?
jasmine.getEnv().currentSpec.fail "Chromedriver exited. code: #{errorCode}. Logs: #{logs.join("\n")}"
resolve(errorCode)
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(
(errorCode) ->
if errorCode?
jasmine.getEnv().currentSpec.fail """
Chromedriver exited with code #{errorCode}.
Logs:\n#{chromedriverLogs.join("\n")}
"""
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}"
"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
pollingInterval = Math.min(timeout, 100)
client.on "error", (err) ->
jasmine.getEnv().currentSpec.fail(JSON.stringify(err))
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)

View File

@ -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,58 @@ 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))
.startAnotherAtom([tempFilePath], ATOM_HOME: AtomHome)
.waitForExist("atom-workspace")
.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))
.waitForNewWindow(->
@startAnotherAtom([otherTempDirPath], ATOM_HOME: AtomHome)
, 5000)
.waitForExist("atom-workspace", 5000)
.waitForPaneItemCount(0, 1000)
it "saves the state of closed windows", ->
runAtom [tempDirPath], {ATOM_HOME: AtomHome}, (client) ->
client
# In a second window, opening a new buffer creates a new tab.
.waitForExist("atom-workspace", 5000)
.waitForNewWindow(->
@startAnotherAtom([otherTempDirPath], ATOM_HOME: AtomHome)
, 5000)
.waitForExist("atom-workspace", 5000)
.waitForPaneItemCount(0, 3000)
.execute(-> atom.workspace.open())
.waitForPaneItemCount(1, 3000)
# Closing that window and reopening that directory shows the
# previously-created new buffer.
.execute(-> atom.unloadEditorWindow())
.close()
.waitForWindowCount(1, 5000)
.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", ->
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.
.startAnotherAtom([tempFilePath], ATOM_HOME: AtomHome)
.waitForExist("atom-workspace", 5000)
.waitForPaneItemCount(1, 5000)
.execute(-> atom.project.getPaths())
.then(({value}) -> expect(value).toEqual([tempDirPath, otherTempDirPath]))

View File

@ -7,6 +7,7 @@ path = require 'path'
BufferedProcess = require '../src/buffered-process'
{Directory} = require 'pathwatcher'
GitRepository = require '../src/git-repository'
temp = require "temp"
describe "Project", ->
beforeEach ->
@ -228,11 +229,33 @@ 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")
gitDirPath = fs.absolute(path.join(__dirname, 'fixtures', 'git', 'master.git'))
fs.copySync(gitDirPath, path.join(directory2, ".git"))
fs.copySync(gitDirPath, path.join(directory3, ".git"))
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"))
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", ->
@ -245,6 +268,53 @@ 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 "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]
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')

View File

@ -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

View File

@ -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
@ -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()
@ -693,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: ->
@ -736,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()

View File

@ -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'
@ -307,9 +311,10 @@ 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, devMode) ->
_.find @windows, (atomWindow) ->
atomWindow.devMode is devMode and atomWindow.containsPaths(pathsToOpen)
# Returns the {AtomWindow} for the given ipc event.
windowForEvent: ({sender}) ->
@ -323,49 +328,39 @@ 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)
existingWindow = @windowForPaths(pathsToOpen, devMode)
# 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
# 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.openPath(pathToOpen, initialLine, initialColumn)
openedWindow.openLocations(locationsToOpen)
if openedWindow.isMinimized()
openedWindow.restore()
else
@ -378,7 +373,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
@ -497,8 +492,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 +521,4 @@ class AtomApplication
openOptions.defaultPath = projectPath
dialog = require 'dialog'
dialog.showOpenDialog parentWindow, openOptions, (pathsToOpen) =>
@openPaths({pathsToOpen, devMode, safeMode, window})
dialog.showOpenDialog(parentWindow, openOptions, callback)

View File

@ -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

View File

@ -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,43 +151,62 @@ 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
@path = 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 now, use only the repositoryProviders with a sync API.
for provider in @repositoryProviders
break if @repo = provider.repositoryForDirectorySync?(@rootDirectory)
else
@rootDirectory = null
@addPath(projectPath, emitEvent: false) for projectPath in projectPaths
@emit "path-changed"
@emitter.emit 'did-change-paths', projectPaths
setPath: (path) ->
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)
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)
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: ->
[@rootDirectory]
@rootDirectories
getRootDirectory: ->
Grim.deprecate("Use ::getDirectories instead")
@rootDirectory
@getDirectories()[0]
resolve: (uri) ->
Grim.deprecate("Use `Project::getDirectories()[0]?.resolve()` instead")
@ -204,6 +220,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
@ -214,7 +232,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.
@ -244,7 +265,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

View File

@ -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)