Merge pull request #19169 from atom/aw/launch-it

Improve launch behavior
This commit is contained in:
Ash Wilson
2019-04-22 20:39:28 -04:00
committed by GitHub
9 changed files with 1858 additions and 1134 deletions

View File

@@ -170,8 +170,8 @@ class ApplicationDelegate {
return ipcRenderer.send('add-recent-document', filename)
}
setRepresentedDirectoryPaths (paths) {
return ipcHelpers.call('window-method', 'setRepresentedDirectoryPaths', paths)
setProjectRoots (paths) {
return ipcHelpers.call('window-method', 'setProjectRoots', paths)
}
setAutoHideWindowMenuBar (autoHide) {

View File

@@ -784,7 +784,9 @@ class AtomEnvironment {
const loadStatePromise = this.loadState().then(async state => {
this.windowDimensions = state && state.windowDimensions
await this.displayWindow()
if (!this.getLoadSettings().headless) {
await this.displayWindow()
}
this.commandInstaller.installAtomCommand(false, (error) => {
if (error) console.warn(error.message)
})
@@ -838,7 +840,7 @@ class AtomEnvironment {
}
}
previousProjectPaths = newPaths
this.applicationDelegate.setRepresentedDirectoryPaths(newPaths)
this.applicationDelegate.setProjectRoots(newPaths)
}))
this.disposables.add(this.workspace.onDidDestroyPaneItem(({item}) => {
const path = item.getPath && item.getPath()
@@ -916,8 +918,8 @@ class AtomEnvironment {
openInitialEmptyEditorIfNecessary () {
if (!this.config.get('core.openEmptyEditorOnStart')) return
const {initialPaths} = this.getLoadSettings()
if (initialPaths && initialPaths.length === 0 && this.workspace.getPaneItems().length === 0) {
const {hasOpenFiles} = this.getLoadSettings()
if (!hasOpenFiles && this.workspace.getPaneItems().length === 0) {
return this.workspace.open(null)
}
}
@@ -1213,7 +1215,7 @@ or use Pane::saveItemAs for programmatic saving.`)
loadState (stateKey) {
if (this.enablePersistence) {
if (!stateKey) stateKey = this.getStateKey(this.getLoadSettings().initialPaths)
if (!stateKey) stateKey = this.getStateKey(this.getLoadSettings().initialProjectRoots)
if (stateKey) {
return this.stateStore.load(stateKey)
} else {
@@ -1388,7 +1390,7 @@ or use Pane::saveItemAs for programmatic saving.`)
// Directory: add as a project folder
foldersToAddToProject.add(this.project.getDirectoryForProjectPath(pathToOpen).getPath())
} else if (stats.isFile()) {
if (location.mustBeDirectory) {
if (location.isDirectory) {
// File: no longer a directory
missingFolders.push(location)
} else {
@@ -1403,7 +1405,7 @@ or use Pane::saveItemAs for programmatic saving.`)
if (directory) {
// Found: add as a project folder
foldersToAddToProject.add(directory.getPath())
} else if (location.mustBeDirectory) {
} else if (location.isDirectory) {
// Not found and must be a directory: add to missing list and use to derive state key
missingFolders.push(location)
} else {

View File

@@ -1,3 +1,5 @@
const fs = require('fs-plus')
// Converts a query string parameter for a line or column number
// to a zero-based line or column number for the Atom API.
function getLineColNumber (numStr) {
@@ -17,7 +19,14 @@ function openFile (atom, {query}) {
function windowShouldOpenFile ({query}) {
const {filename} = query
return (win) => win.containsPath(filename)
const stat = fs.statSyncNoException(filename)
return win => win.containsLocation({
pathToOpen: filename,
exists: Boolean(stat),
isFile: stat.isFile(),
isDirectory: stat.isDirectory()
})
}
const ROUTER = {
@@ -39,7 +48,7 @@ module.exports = {
if (config && config.getWindowPredicate) {
return config.getWindowPredicate(parsed)
} else {
return (win) => true
return () => true
}
}
}

View File

@@ -38,6 +38,8 @@ exports.respondTo = function (channel, callback) {
return exports.on(ipcMain, channel, async (event, responseChannel, ...args) => {
const browserWindow = BrowserWindow.fromWebContents(event.sender)
const result = await callback(browserWindow, ...args)
event.sender.send(responseChannel, result)
if (!event.sender.isDestroyed()) {
event.sender.send(responseChannel, result)
}
})
}

View File

@@ -120,6 +120,11 @@ class AtomApplication extends EventEmitter {
static open (options) {
const socketSecret = getExistingSocketSecret(options.version)
const socketPath = getSocketPath(socketSecret)
const createApplication = options.createApplication || (async () => {
const app = new AtomApplication(options)
await app.initialize(options)
return app
})
// FIXME: Sometimes when socketPath doesn't exist, net.connect would strangely
// take a few seconds to trigger 'error' event, it could be a bug of node
@@ -129,18 +134,20 @@ class AtomApplication extends EventEmitter {
!socketPath || options.test || options.benchmark || options.benchmarkTest ||
(process.platform !== 'win32' && !fs.existsSync(socketPath))
) {
new AtomApplication(options).initialize(options)
return
return createApplication(options)
}
const client = net.connect({path: socketPath}, () => {
client.write(encryptOptions(options, socketSecret), () => {
client.end()
app.quit()
return new Promise(resolve => {
const client = net.connect({path: socketPath}, () => {
client.write(encryptOptions(options, socketSecret), () => {
client.end()
app.quit()
resolve(null)
})
})
})
client.on('error', () => new AtomApplication(options).initialize(options))
client.on('error', () => resolve(createApplication(options)))
})
}
exit (status) {
@@ -246,19 +253,19 @@ class AtomApplication extends EventEmitter {
if (options.test || options.benchmark || options.benchmarkTest) {
optionsForWindowsToOpen.push(options)
} else if (options.newWindow) {
shouldReopenPreviousWindows = false
} else if ((options.pathsToOpen && options.pathsToOpen.length > 0) ||
(options.urlsToOpen && options.urlsToOpen.length > 0)) {
optionsForWindowsToOpen.push(options)
shouldReopenPreviousWindows = this.config.get('core.restorePreviousWindowsOnStart') === 'always'
} else if (options.newWindow) {
shouldReopenPreviousWindows = false
} else {
shouldReopenPreviousWindows = this.config.get('core.restorePreviousWindowsOnStart') !== 'no'
}
if (shouldReopenPreviousWindows) {
for (const previousOptions of await this.loadPreviousWindowOptions()) {
optionsForWindowsToOpen.push(Object.assign({}, options, previousOptions))
optionsForWindowsToOpen.push(previousOptions)
}
}
@@ -266,7 +273,12 @@ class AtomApplication extends EventEmitter {
optionsForWindowsToOpen.push(options)
}
return optionsForWindowsToOpen.map(options => this.openWithOptions(options))
// Preserve window opening order
const windows = []
for (const options of optionsForWindowsToOpen) {
windows.push(await this.openWithOptions(options))
}
return windows
}
openWithOptions (options) {
@@ -287,10 +299,13 @@ class AtomApplication extends EventEmitter {
timeout,
clearWindowState,
addToLastWindow,
preserveFocus,
env
} = options
app.focus()
if (!preserveFocus) {
app.focus()
}
if (test) {
return this.runTests({
@@ -327,11 +342,14 @@ class AtomApplication extends EventEmitter {
addToLastWindow,
env
})
} else if (urlsToOpen.length > 0) {
return urlsToOpen.map(urlToOpen => this.openUrl({urlToOpen, devMode, safeMode, env}))
} else if (urlsToOpen && urlsToOpen.length > 0) {
return Promise.all(
urlsToOpen.map(urlToOpen => this.openUrl({urlToOpen, devMode, safeMode, env}))
)
} else {
// Always open a editor window if this is the first instance of Atom.
// Always open an editor window if this is the first instance of Atom.
return this.openPath({
pathToOpen: null,
pidToKillWhenClosed,
newWindow,
devMode,
@@ -344,6 +362,11 @@ class AtomApplication extends EventEmitter {
}
}
// Public: Create a new {AtomWindow} bound to this application.
createWindow (settings) {
return new AtomWindow(this, this.fileRecoveryService, settings)
}
// Public: Removes the {AtomWindow} from the global window list.
removeWindow (window) {
this.windowStack.removeWindow(window)
@@ -379,6 +402,7 @@ class AtomApplication extends EventEmitter {
window.browserWindow.removeListener('blur', blurHandler)
})
window.browserWindow.webContents.once('did-finish-load', blurHandler)
this.saveCurrentWindowOptions(false)
}
}
@@ -413,8 +437,10 @@ class AtomApplication extends EventEmitter {
})
})
server.listen(this.socketPath)
server.on('error', error => console.error('Application server failed', error))
return new Promise(resolve => {
server.listen(this.socketPath, resolve)
server.on('error', error => console.error('Application server failed', error))
})
}
deleteSocketFile () {
@@ -455,9 +481,9 @@ class AtomApplication extends EventEmitter {
const getLoadSettings = includeWindow => {
const window = this.focusedWindow()
return {
devMode: window && window.devMode,
safeMode: window && window.safeMode,
window: includeWindow && window
devMode: window ? window.devMode : false,
safeMode: window ? window.safeMode : false,
window: includeWindow && window ? window : null
}
}
@@ -609,7 +635,7 @@ class AtomApplication extends EventEmitter {
options.window = window
this.openPaths(options)
} else {
this.addWindow(new AtomWindow(this, this.fileRecoveryService, options))
this.addWindow(this.createWindow(options))
}
} else {
this.promptForPathToOpen('all', {window})
@@ -657,7 +683,7 @@ class AtomApplication extends EventEmitter {
this.disposable.add(ipcHelpers.on(ipcMain, 'window-command', (event, command, ...args) => {
const window = BrowserWindow.fromWebContents(event.sender)
return window.emit(command, ...args)
return window && window.emit(command, ...args)
}))
this.disposable.add(ipcHelpers.respondTo('window-method', (browserWindow, method, ...args) => {
@@ -729,10 +755,6 @@ class AtomApplication extends EventEmitter {
this.fileRecoveryService.didSavePath(window, path)
))
this.disposable.add(ipcHelpers.on(ipcMain, 'did-change-paths', () =>
this.saveCurrentWindowOptions(false)
))
this.disposable.add(this.disableZoomOnDisplayChange())
}
@@ -831,10 +853,12 @@ class AtomApplication extends EventEmitter {
})
}
// Returns the {AtomWindow} for the given paths.
windowForPaths (pathsToOpen, devMode) {
return this.getAllWindows().find(window =>
window.devMode === devMode && window.containsPaths(pathsToOpen)
// Returns the {AtomWindow} for the given locations.
windowForLocations (locationsToOpen, devMode, safeMode) {
return this.getLastFocusedWindow(window =>
window.devMode === devMode &&
window.safeMode === safeMode &&
window.containsLocations(locationsToOpen)
)
}
@@ -924,7 +948,7 @@ class AtomApplication extends EventEmitter {
// :windowDimensions - Object with height and width keys.
// :window - {AtomWindow} to open file paths in.
// :addToLastWindow - Boolean of whether this should be opened in last focused window.
openPaths ({
async openPaths ({
pathsToOpen,
foldersToOpen,
executedFrom,
@@ -947,20 +971,19 @@ class AtomApplication extends EventEmitter {
safeMode = Boolean(safeMode)
clearWindowState = Boolean(clearWindowState)
const locationsToOpen = pathsToOpen.map(pathToOpen => {
return this.parsePathToOpen(pathToOpen, executedFrom, {
forceAddToWindow: addToLastWindow,
const locationsToOpen = await Promise.all(
pathsToOpen.map(pathToOpen => this.parsePathToOpen(pathToOpen, executedFrom, {
hasWaitSession: pidToKillWhenClosed != null
})
})
}))
)
for (const folderToOpen of foldersToOpen) {
locationsToOpen.push({
pathToOpen: folderToOpen,
initialLine: null,
initialColumn: null,
mustBeDirectory: true,
forceAddToWindow: addToLastWindow,
exists: true,
isDirectory: true,
hasWaitSession: pidToKillWhenClosed != null
})
}
@@ -969,27 +992,42 @@ class AtomApplication extends EventEmitter {
return
}
const normalizedPathsToOpen = locationsToOpen.map(location => location.pathToOpen).filter(Boolean)
const hasNonEmptyPath = locationsToOpen.some(location => location.pathToOpen)
const createNewWindow = newWindow || !hasNonEmptyPath
let existingWindow
// Explicitly provided AtomWindow has precedence unless a new window is forced.
if (!newWindow) {
if (!createNewWindow) {
// An explicitly provided AtomWindow has precedence.
existingWindow = window
}
// If no window is specified, a new window is not forced, and at least one path is provided, locate
// an existing window that contains all paths.
if (!existingWindow && !newWindow && normalizedPathsToOpen.length > 0) {
existingWindow = this.windowForPaths(normalizedPathsToOpen, devMode)
}
// If no window is specified and at least one path is provided, locate an existing window that contains all
// provided paths.
if (!existingWindow && hasNonEmptyPath) {
existingWindow = this.windowForLocations(locationsToOpen, devMode, safeMode)
}
// No window specified, new window not forced, no existing window found, and addition to the last window
// requested. Find the last focused window.
if (!existingWindow && !newWindow && addToLastWindow) {
let lastWindow = window || this.getLastFocusedWindow()
if (lastWindow && lastWindow.devMode === devMode) {
existingWindow = lastWindow
// No window specified, no existing window found, and addition to the last window requested. Find the last
// focused window that matches the requested dev and safe modes.
if (!existingWindow && addToLastWindow) {
existingWindow = this.getLastFocusedWindow(win => {
return win.devMode === devMode && win.safeMode === safeMode
})
}
// Fall back to the last focused window that has no project roots.
if (!existingWindow) {
existingWindow = this.getLastFocusedWindow(win => !win.hasProjectPaths())
}
// One last case: if *no* paths are directories, add to the last focused window.
if (!existingWindow) {
const noDirectories = locationsToOpen.every(location => !location.isDirectory)
if (noDirectories) {
existingWindow = this.getLastFocusedWindow(win => {
return win.devMode === devMode && win.safeMode === safeMode
})
}
}
}
@@ -1020,7 +1058,7 @@ class AtomApplication extends EventEmitter {
if (!resourcePath) resourcePath = this.resourcePath
if (!windowDimensions) windowDimensions = this.getDimensionsForNewWindow()
openedWindow = new AtomWindow(this, this.fileRecoveryService, {
openedWindow = this.createWindow({
locationsToOpen,
windowInitializationScript,
resourcePath,
@@ -1041,7 +1079,7 @@ class AtomApplication extends EventEmitter {
}
this.waitSessionsByWindow.get(openedWindow).push({
pid: pidToKillWhenClosed,
remainingPaths: new Set(normalizedPathsToOpen)
remainingPaths: new Set(locationsToOpen.map(location => location.pathToOpen).filter(Boolean))
})
}
@@ -1095,7 +1133,7 @@ class AtomApplication extends EventEmitter {
const states = []
for (let window of this.getAllWindows()) {
if (!window.isSpec) states.push({initialPaths: window.representedDirectoryPaths})
if (!window.isSpec) states.push({initialPaths: window.projectRoots})
}
states.reverse()
@@ -1110,7 +1148,6 @@ class AtomApplication extends EventEmitter {
if (states) {
return states.map(state => ({
foldersToOpen: state.initialPaths,
urlsToOpen: [],
devMode: this.devMode,
safeMode: this.safeMode
}))
@@ -1161,6 +1198,7 @@ class AtomApplication extends EventEmitter {
if (bestWindow) {
bestWindow.sendURIMessage(url)
bestWindow.focus()
return bestWindow
} else {
let windowInitializationScript
let {resourcePath} = this
@@ -1178,7 +1216,7 @@ class AtomApplication extends EventEmitter {
}
const windowDimensions = this.getDimensionsForNewWindow()
const window = new AtomWindow(this, this.fileRecoveryService, {
const window = this.createWindow({
resourcePath,
windowInitializationScript,
devMode,
@@ -1202,7 +1240,7 @@ class AtomApplication extends EventEmitter {
const packagePath = this.getPackageManager(devMode).resolvePackagePath(packageName)
const windowInitializationScript = path.resolve(packagePath, packageUrlMain)
const windowDimensions = this.getDimensionsForNewWindow()
const window = new AtomWindow(this, this.fileRecoveryService, {
const window = this.createWindow({
windowInitializationScript,
resourcePath: this.resourcePath,
devMode,
@@ -1284,7 +1322,7 @@ class AtomApplication extends EventEmitter {
if (safeMode == null) {
safeMode = false
}
const window = new AtomWindow(this, this.fileRecoveryService, {
const window = this.createWindow({
windowInitializationScript,
resourcePath,
headless,
@@ -1333,7 +1371,7 @@ class AtomApplication extends EventEmitter {
const devMode = true
const isSpec = true
const safeMode = false
const window = new AtomWindow(this, this.fileRecoveryService, {
const window = this.createWindow({
windowInitializationScript,
resourcePath,
headless,
@@ -1388,31 +1426,58 @@ class AtomApplication extends EventEmitter {
}
}
parsePathToOpen (pathToOpen, executedFrom, extra) {
let initialColumn, initialLine
async parsePathToOpen (pathToOpen, executedFrom, extra) {
const result = Object.assign({
pathToOpen,
initialColumn: null,
initialLine: null,
exists: false,
isDirectory: false,
isFile: false
}, extra)
if (!pathToOpen) {
return {pathToOpen}
return result
}
pathToOpen = pathToOpen.replace(/[:\s]+$/, '')
const match = pathToOpen.match(LocationSuffixRegExp)
result.pathToOpen = result.pathToOpen.replace(/[:\s]+$/, '')
const match = result.pathToOpen.match(LocationSuffixRegExp)
if (match != null) {
pathToOpen = pathToOpen.slice(0, -match[0].length)
result.pathToOpen = result.pathToOpen.slice(0, -match[0].length)
if (match[1]) {
initialLine = Math.max(0, parseInt(match[1].slice(1)) - 1)
result.initialLine = Math.max(0, parseInt(match[1].slice(1), 10) - 1)
}
if (match[2]) {
initialColumn = Math.max(0, parseInt(match[2].slice(1)) - 1)
result.initialColumn = Math.max(0, parseInt(match[2].slice(1), 10) - 1)
}
} else {
initialLine = initialColumn = null
}
const normalizedPath = path.normalize(path.resolve(executedFrom, fs.normalize(pathToOpen)))
if (!url.parse(pathToOpen).protocol) pathToOpen = normalizedPath
const normalizedPath = path.normalize(path.resolve(executedFrom, fs.normalize(result.pathToOpen)))
if (!url.parse(pathToOpen).protocol) {
result.pathToOpen = normalizedPath
}
return Object.assign({pathToOpen, initialLine, initialColumn}, extra)
await new Promise((resolve, reject) => {
fs.stat(result.pathToOpen, (err, st) => {
if (err) {
if (err.code === 'ENOENT' || err.code === 'EACCES') {
result.exists = false
resolve()
} else {
reject(err)
}
return
}
result.exists = true
result.isFile = st.isFile()
result.isDirectory = st.isDirectory()
resolve()
})
})
return result
}
// Opens a native dialog to prompt the user for a path.
@@ -1467,8 +1532,8 @@ class AtomApplication extends EventEmitter {
}
})()
// Show the open dialog as child window on Windows and Linux, and as
// independent dialog on macOS. This matches most native apps.
// Show the open dialog as child window on Windows and Linux, and as an independent dialog on macOS. This matches
// most native apps.
const parentWindow = process.platform === 'darwin' ? null : BrowserWindow.getFocusedWindow()
const openOptions = {

View File

@@ -1,6 +1,5 @@
const {BrowserWindow, app, dialog, ipcMain} = require('electron')
const path = require('path')
const fs = require('fs')
const url = require('url')
const {EventEmitter} = require('events')
@@ -51,7 +50,9 @@ class AtomWindow extends EventEmitter {
if (this.shouldAddCustomTitleBar()) options.titleBarStyle = 'hidden'
if (this.shouldAddCustomInsetTitleBar()) options.titleBarStyle = 'hiddenInset'
if (this.shouldHideTitleBar()) options.frame = false
this.browserWindow = new BrowserWindow(options)
const BrowserWindowConstructor = settings.browserWindowConstructor || BrowserWindow
this.browserWindow = new BrowserWindowConstructor(options)
Object.defineProperty(this.browserWindow, 'loadSettingsJSON', {
get: () => JSON.stringify(Object.assign({
@@ -71,8 +72,14 @@ class AtomWindow extends EventEmitter {
if (this.loadSettings.safeMode == null) this.loadSettings.safeMode = false
if (this.loadSettings.clearWindowState == null) this.loadSettings.clearWindowState = false
this.loadSettings.initialPaths = locationsToOpen.map(location => location.pathToOpen).filter(Boolean)
this.loadSettings.initialPaths.sort()
this.projectRoots = locationsToOpen
.filter(location => location.pathToOpen && location.exists && location.isDirectory)
.map(location => location.pathToOpen)
this.projectRoots.sort()
this.loadSettings.hasOpenFiles = locationsToOpen
.some(location => location.pathToOpen && !location.isDirectory)
this.loadSettings.initialProjectRoots = this.projectRoots
// Only send to the first non-spec window created
if (includeShellLoadTime && !this.isSpec) {
@@ -82,7 +89,6 @@ class AtomWindow extends EventEmitter {
}
}
this.representedDirectoryPaths = this.loadSettings.initialPaths
if (!this.loadSettings.env) this.env = this.loadSettings.env
this.browserWindow.on('window:loaded', () => {
@@ -119,8 +125,8 @@ class AtomWindow extends EventEmitter {
if (hasPathToOpen && !this.isSpecWindow()) this.openLocations(locationsToOpen)
}
hasProjectPath () {
return this.representedDirectoryPaths.length > 0
hasProjectPaths () {
return this.projectRoots.length > 0
}
setupContextMenu () {
@@ -131,18 +137,20 @@ class AtomWindow extends EventEmitter {
})
}
containsPaths (paths) {
return paths.every(p => this.containsPath(p))
containsLocations (locations) {
return locations.every(location => this.containsLocation(location))
}
containsPath (pathToCheck) {
if (!pathToCheck) return false
let stat
return this.representedDirectoryPaths.some(projectPath => {
if (pathToCheck === projectPath) return true
if (!pathToCheck.startsWith(path.join(projectPath, path.sep))) return false
if (stat === undefined) stat = fs.statSyncNoException(pathToCheck)
return !stat || !stat.isDirectory()
containsLocation (location) {
if (!location.pathToOpen) return false
return this.projectRoots.some(projectPath => {
if (location.pathToOpen === projectPath) return true
if (location.pathToOpen.startsWith(path.join(projectPath, path.sep))) {
if (!location.exists) return true
if (!location.isDirectory) return true
}
return false
})
}
@@ -376,7 +384,7 @@ class AtomWindow extends EventEmitter {
showSaveDialog (options, callback) {
options = Object.assign({
title: 'Save File',
defaultPath: this.representedDirectoryPaths[0]
defaultPath: this.projectRoots[0]
}, options)
if (typeof callback === 'function') {
@@ -408,10 +416,10 @@ class AtomWindow extends EventEmitter {
return this.browserWindow.setRepresentedFilename(representedFilename)
}
setRepresentedDirectoryPaths (representedDirectoryPaths) {
this.representedDirectoryPaths = representedDirectoryPaths
this.representedDirectoryPaths.sort()
this.loadSettings.initialPaths = this.representedDirectoryPaths
setProjectRoots (projectRootPaths) {
this.projectRoots = projectRootPaths
this.projectRoots.sort()
this.loadSettings.initialProjectRoots = this.projectRoots
return this.atomApplication.saveCurrentWindowOptions()
}
@@ -426,4 +434,8 @@ class AtomWindow extends EventEmitter {
disableZoom () {
return this.browserWindow.webContents.setVisualZoomLevelLimits(1, 1)
}
getLoadedPromise () {
return this.loadedPromise
}
}