Skip to content
Permalink
Browse files

Merge pull request #19203 from atom/aw/reopen-projects

"Reopen projects" in a new window
  • Loading branch information...
smashwilson committed Apr 23, 2019
1 parent 41caa4a commit d79c3bae865b40079909f6b5acc5cb75a5fd3e5e
@@ -745,6 +745,106 @@ describe('AtomApplication', function() {
})
})

describe('IPC handling', function() {
let w0, w1, w2, app

beforeEach(async function() {
w0 = (await scenario.launch(parseCommandLine(['a'])))[0]
w1 = await scenario.open(parseCommandLine(['--new-window']))
w2 = await scenario.open(parseCommandLine(['--new-window', 'b']))

app = scenario.getApplication(0)
sinon.spy(app, 'openPaths')
sinon.stub(app, 'promptForPath', (_type, callback, defaultPath) => callback([defaultPath]))
})

// This is the IPC message used to handle:
// * application:reopen-project
// * choosing "open in new window" when adding a folder that has previously saved state
// * deprecated call links in deprecation-cop
// * other direct callers of `atom.open()`
it('"open" opens a fixed path by the standard opening rules', async function() {
sinon.stub(app, 'atomWindowForEvent', () => w1)

electron.ipcMain.emit('open', {}, { pathsToOpen: scenario.convertEditorPath('a/1.md') })
await app.openPaths.lastCall.returnValue
await scenario.assert('[a 1.md] [_ _] [b _]')

electron.ipcMain.emit('open', {}, { pathsToOpen: scenario.convertRootPath('c') })
await app.openPaths.lastCall.returnValue
await scenario.assert('[a 1.md] [c _] [b _]')

electron.ipcMain.emit('open', {}, { pathsToOpen: scenario.convertRootPath('d') })
await app.openPaths.lastCall.returnValue
await scenario.assert('[a 1.md] [c _] [b _] [d _]')
})

it('"open-chosen-any" opens a file in the sending window', async function() {
sinon.stub(app, 'atomWindowForEvent', () => w2)

electron.ipcMain.emit('open-chosen-any', {}, scenario.convertEditorPath('a/1.md'))
await conditionPromise(() => app.openPaths.called)
await app.openPaths.lastCall.returnValue
await scenario.assert('[a _] [_ _] [b 1.md]')

assert.isTrue(app.promptForPath.called)
assert.strictEqual(app.promptForPath.lastCall.args[0], 'all')
})

it('"open-chosen-any" opens a directory by the standard opening rules', async function() {
sinon.stub(app, 'atomWindowForEvent', () => w1)

// Open unrecognized directory in empty window
electron.ipcMain.emit('open-chosen-any', {}, scenario.convertRootPath('c'))
await conditionPromise(() => app.openPaths.callCount > 0)
await app.openPaths.lastCall.returnValue
await scenario.assert('[a _] [c _] [b _]')

assert.strictEqual(app.promptForPath.callCount, 1)
assert.strictEqual(app.promptForPath.lastCall.args[0], 'all')

// Open unrecognized directory in new window
electron.ipcMain.emit('open-chosen-any', {}, scenario.convertRootPath('d'))
await conditionPromise(() => app.openPaths.callCount > 1)
await app.openPaths.lastCall.returnValue
await scenario.assert('[a _] [c _] [b _] [d _]')

assert.strictEqual(app.promptForPath.callCount, 2)
assert.strictEqual(app.promptForPath.lastCall.args[0], 'all')

// Open recognized directory in existing window
electron.ipcMain.emit('open-chosen-any', {}, scenario.convertRootPath('a'))
await conditionPromise(() => app.openPaths.callCount > 2)
await app.openPaths.lastCall.returnValue
await scenario.assert('[a _] [c _] [b _] [d _]')

assert.strictEqual(app.promptForPath.callCount, 3)
assert.strictEqual(app.promptForPath.lastCall.args[0], 'all')
})

it('"open-chosen-file" opens a file chooser and opens the chosen file in the sending window', async function() {
sinon.stub(app, 'atomWindowForEvent', () => w0)

electron.ipcMain.emit('open-chosen-file', {}, scenario.convertEditorPath('b/2.md'))
await app.openPaths.lastCall.returnValue
await scenario.assert('[a 2.md] [_ _] [b _]')

assert.isTrue(app.promptForPath.called)
assert.strictEqual(app.promptForPath.lastCall.args[0], 'file')
})

it('"open-chosen-folder" opens a directory chooser and opens the chosen directory', async function() {
sinon.stub(app, 'atomWindowForEvent', () => w0)

electron.ipcMain.emit('open-chosen-folder', {}, scenario.convertRootPath('c'))
await app.openPaths.lastCall.returnValue
await scenario.assert('[a _] [c _] [b _]')

assert.isTrue(app.promptForPath.called)
assert.strictEqual(app.promptForPath.lastCall.args[0], 'folder')
})
})

describe('window state serialization', function() {
it('occurs immediately when adding a window', async function() {
await scenario.launch(parseCommandLine(['a']))
@@ -1027,7 +1127,7 @@ class LaunchScenario {
process.env.ATOM_HOME = this.atomHome

await Promise.all(
['a', 'b', 'c'].map(dirPath => new Promise((resolve, reject) => {
['a', 'b', 'c', 'd'].map(dirPath => new Promise((resolve, reject) => {
const fullDirPath = path.join(this.root, dirPath)
fs.makeTree(fullDirPath, err => {
if (err) {
@@ -306,7 +306,14 @@ class AtomEnvironment {
}

registerDefaultCommands () {
registerDefaultCommands({commandRegistry: this.commands, config: this.config, commandInstaller: this.commandInstaller, notificationManager: this.notifications, project: this.project, clipboard: this.clipboard})
registerDefaultCommands({
commandRegistry: this.commands,
config: this.config,
commandInstaller: this.commandInstaller,
notificationManager: this.notifications,
project: this.project,
clipboard: this.clipboard
})
}

registerDefaultOpeners () {
@@ -478,17 +478,17 @@ class AtomApplication extends EventEmitter {

// Registers basic application commands, non-idempotent.
handleEvents () {
const getLoadSettings = includeWindow => {
const window = this.focusedWindow()
const createOpenSettings = ({event, sameWindow}) => {
const targetWindow = event ? this.atomWindowForEvent(event) : this.focusedWindow()
return {
devMode: window ? window.devMode : false,
safeMode: window ? window.safeMode : false,
window: includeWindow && window ? window : null
devMode: targetWindow ? targetWindow.devMode : false,
safeMode: targetWindow ? targetWindow.safeMode : false,
window: sameWindow && targetWindow ? targetWindow : null
}
}

this.on('application:quit', () => app.quit())
this.on('application:new-window', () => this.openPath(getLoadSettings(false)))
this.on('application:new-window', () => this.openPath(createOpenSettings({})))
this.on('application:new-file', () => (this.focusedWindow() || this).openPath())
this.on('application:open-dev', () => this.promptForPathToOpen('all', {devMode: true}))
this.on('application:open-safe', () => this.promptForPathToOpen('all', {safeMode: true}))
@@ -517,9 +517,16 @@ class AtomApplication extends EventEmitter {
this.openPaths({ pathsToOpen: paths })
})

this.on('application:open', () => this.promptForPathToOpen('all', getLoadSettings(true), getDefaultPath()))
this.on('application:open-file', () => this.promptForPathToOpen('file', getLoadSettings(true), getDefaultPath()))
this.on('application:open-folder', () => this.promptForPathToOpen('folder', getLoadSettings(true), getDefaultPath()))
this.on('application:open', () => {
this.promptForPathToOpen('all', createOpenSettings({sameWindow: true}), getDefaultPath())
})
this.on('application:open-file', () => {
this.promptForPathToOpen('file', createOpenSettings({sameWindow: true}), getDefaultPath())
})
this.on('application:open-folder', () => {
this.promptForPathToOpen('folder', createOpenSettings({sameWindow: true}), getDefaultPath())
})

this.on('application:bring-all-windows-to-front', () => Menu.sendActionToFirstResponder('arrangeInFront:'))
this.on('application:hide', () => Menu.sendActionToFirstResponder('hide:'))
this.on('application:hide-other-applications', () => Menu.sendActionToFirstResponder('hideOtherApplications:'))
@@ -590,6 +597,9 @@ class AtomApplication extends EventEmitter {
this.deleteSocketSecretFile()
}))

// Triggered by the 'open-file' event from Electron:
// https://electronjs.org/docs/api/app#event-open-file-macos
// For example, this is fired when a file is dragged and dropped onto the Atom application icon in the dock.
this.disposable.add(ipcHelpers.on(app, 'open-file', (event, pathToOpen) => {
event.preventDefault()
this.openPath({pathToOpen})
@@ -623,16 +633,15 @@ class AtomApplication extends EventEmitter {
}
}))

// A request from the associated render process to open a new render process.
this.disposable.add(ipcHelpers.on(ipcMain, 'open', (event, options) => {
const window = this.atomWindowForEvent(event)
// A request from the associated render process to open a set of paths using the standard window location logic.
// Used for application:reopen-project.
this.disposable.add(ipcHelpers.on(ipcMain, 'open', (_event, options) => {
if (options) {
if (typeof options.pathsToOpen === 'string') {
options.pathsToOpen = [options.pathsToOpen]
}

if (options.pathsToOpen && options.pathsToOpen.length > 0) {
options.window = window
this.openPaths(options)
} else {
this.addWindow(this.createWindow(options))
@@ -642,6 +651,18 @@ class AtomApplication extends EventEmitter {
}
}))

// Prompt for a file, folder, or either, then open the chosen paths. Files will be opened in the originating
// window; folders will be opened in a new window unless an existing window exactly contains all of them.
this.disposable.add(ipcHelpers.on(ipcMain, 'open-chosen-any', (event, defaultPath) => {
this.promptForPathToOpen('all', createOpenSettings({event, sameWindow: true}), defaultPath)
}))
this.disposable.add(ipcHelpers.on(ipcMain, 'open-chosen-file', (event, defaultPath) => {
this.promptForPathToOpen('file', createOpenSettings({event, sameWindow: true}), defaultPath)
}))
this.disposable.add(ipcHelpers.on(ipcMain, 'open-chosen-folder', (event, defaultPath) => {
this.promptForPathToOpen('folder', createOpenSettings({event}), defaultPath)
}))

this.disposable.add(ipcHelpers.on(ipcMain, 'update-application-menu', (event, template, menu) => {
const window = BrowserWindow.fromWebContents(event.sender)
if (this.applicationMenu) this.applicationMenu.update(window, template, menu)
@@ -668,19 +689,6 @@ class AtomApplication extends EventEmitter {
this.emit(command)
}))

this.disposable.add(ipcHelpers.on(ipcMain, 'open-command', (event, command, defaultPath) => {
switch (command) {
case 'application:open':
return this.promptForPathToOpen('all', getLoadSettings(true), defaultPath)
case 'application:open-file':
return this.promptForPathToOpen('file', getLoadSettings(true), defaultPath)
case 'application:open-folder':
return this.promptForPathToOpen('folder', getLoadSettings(true), defaultPath)
default:
return console.log(`Invalid open-command received: ${command}`)
}
}))

this.disposable.add(ipcHelpers.on(ipcMain, 'window-command', (event, command, ...args) => {
const window = BrowserWindow.fromWebContents(event.sender)
return window && window.emit(command, ...args)
@@ -36,13 +36,13 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage
'application:new-file': -> ipcRenderer.send('command', 'application:new-file')
'application:open': ->
defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0]
ipcRenderer.send('open-command', 'application:open', defaultPath)
ipcRenderer.send('open-chosen-any', defaultPath)
'application:open-file': ->
defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0]
ipcRenderer.send('open-command', 'application:open-file', defaultPath)
ipcRenderer.send('open-chosen-file', defaultPath)
'application:open-folder': ->
defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0]
ipcRenderer.send('open-command', 'application:open-folder', defaultPath)
ipcRenderer.send('open-chosen-folder', defaultPath)
'application:open-dev': -> ipcRenderer.send('command', 'application:open-dev')
'application:open-safe': -> ipcRenderer.send('command', 'application:open-safe')
'application:add-project-folder': -> atom.addProjectFolder()

0 comments on commit d79c3ba

Please sign in to comment.
You can’t perform that action at this time.