Merge branch 'master' into autoFocus-element

This commit is contained in:
Rafael Oleza
2019-04-26 20:15:06 +02:00
193 changed files with 22549 additions and 9758 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

@@ -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 () {
@@ -784,7 +791,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 +847,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 +925,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 +1222,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 {
@@ -1238,9 +1247,8 @@ or use Pane::saveItemAs for programmatic saving.`)
try {
await this.project.deserialize(state.project, this.deserializers)
} catch (error) {
if (error.missingProjectPaths) {
missingProjectPaths.push(...error.missingProjectPaths)
} else {
// We handle the missingProjectPaths case in openLocations().
if (!error.missingProjectPaths) {
this.notifications.addError('Unable to deserialize project', {
description: error.message,
stack: error.stack
@@ -1259,7 +1267,7 @@ or use Pane::saveItemAs for programmatic saving.`)
if (missingProjectPaths.length > 0) {
const count = missingProjectPaths.length === 1 ? '' : missingProjectPaths.length + ' '
const noun = missingProjectPaths.length === 1 ? 'directory' : 'directories'
const noun = missingProjectPaths.length === 1 ? 'folder' : 'folders'
const toBe = missingProjectPaths.length === 1 ? 'is' : 'are'
const escaped = missingProjectPaths.map(projectPath => `\`${projectPath}\``)
let group
@@ -1364,6 +1372,7 @@ or use Pane::saveItemAs for programmatic saving.`)
const needsProjectPaths = this.project && this.project.getPaths().length === 0
const foldersToAddToProject = new Set()
const fileLocationsToOpen = []
const missingFolders = []
// Asynchronously fetch stat information about each requested path to open.
const locationStats = await Promise.all(
@@ -1376,6 +1385,8 @@ or use Pane::saveItemAs for programmatic saving.`)
for (const {location, stats} of locationStats) {
const {pathToOpen} = location
if (!pathToOpen) {
// Untitled buffer
fileLocationsToOpen.push(location)
continue
}
@@ -1385,8 +1396,13 @@ or use Pane::saveItemAs for programmatic saving.`)
// Directory: add as a project folder
foldersToAddToProject.add(this.project.getDirectoryForProjectPath(pathToOpen).getPath())
} else if (stats.isFile()) {
// File: add as a file location
fileLocationsToOpen.push(location)
if (location.isDirectory) {
// File: no longer a directory
missingFolders.push(location)
} else {
// File: add as a file location
fileLocationsToOpen.push(location)
}
}
} else {
// Path does not exist
@@ -1395,6 +1411,9 @@ or use Pane::saveItemAs for programmatic saving.`)
if (directory) {
// Found: add as a project folder
foldersToAddToProject.add(directory.getPath())
} 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 {
// Not found: open as a new file
fileLocationsToOpen.push(location)
@@ -1405,8 +1424,12 @@ or use Pane::saveItemAs for programmatic saving.`)
}
let restoredState = false
if (foldersToAddToProject.size > 0) {
const state = await this.loadState(this.getStateKey(Array.from(foldersToAddToProject)))
if (foldersToAddToProject.size > 0 || missingFolders.length > 0) {
// Include missing folders in the state key so that sessions restored with no-longer-present project root folders
// don't lose data.
const foldersForStateKey = Array.from(foldersToAddToProject)
.concat(missingFolders.map(location => location.pathToOpen))
const state = await this.loadState(this.getStateKey(Array.from(foldersForStateKey)))
// only restore state if this is the first path added to the project
if (state && needsProjectPaths) {
@@ -1428,6 +1451,33 @@ or use Pane::saveItemAs for programmatic saving.`)
await Promise.all(fileOpenPromises)
}
if (missingFolders.length > 0) {
let message = 'Unable to open project folder'
if (missingFolders.length > 1) {
message += 's'
}
let description = 'The '
if (missingFolders.length === 1) {
description += 'directory `'
description += missingFolders[0].pathToOpen
description += '` does not exist.'
} else if (missingFolders.length === 2) {
description += `directories \`${missingFolders[0].pathToOpen}\` `
description += `and \`${missingFolders[1].pathToOpen}\` do not exist.`
} else {
description += 'directories '
description += (missingFolders
.slice(0, -1)
.map(location => location.pathToOpen)
.map(pathToOpen => '`' + pathToOpen + '`, ')
.join(''))
description += 'and `' + missingFolders[missingFolders.length - 1].pathToOpen + '` do not exist.'
}
this.notifications.addWarning(message, {description})
}
ipcRenderer.send('window-command', 'window:locations-opened')
}

View File

@@ -265,6 +265,22 @@ const schemaEnforcers = {}
// ]
// ```
//
// If you only have a few elements, you can display your enum as a list of
// radio buttons in the settings view rather than a select list. To do so,
// specify `radio: true` as a sibling property to the `enum` array.
//
// ```coffee
// config:
// someSetting:
// type: 'string'
// default: 'foo'
// enum: [
// {value: 'foo', description: 'Foo mode. You want this.'}
// {value: 'bar', description: 'Bar mode. Nobody wants that!'}
// ]
// radio: true
// ```
//
// Usage:
//
// ```coffee
@@ -691,7 +707,9 @@ class Config {
this.pendingOperations.push(() => this.set(keyPath, value, options))
}
const scopeSelector = options.scopeSelector
// We should never use the scoped store to set global settings, since they are kept directly
// in the config object.
const scopeSelector = options.scopeSelector !== '*' ? options.scopeSelector : undefined
let source = options.source
const shouldSave = options.save != null ? options.save : true

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

@@ -3,12 +3,17 @@ const {Emitter} = require('event-kit')
let idCounter = 0
const nextId = () => idCounter++
// Applies changes to a decorationsParam {Object} to make it possible to
// differentiate decorations on custom gutters versus the line-number gutter.
const translateDecorationParamsOldToNew = function (decorationParams) {
if (decorationParams.type === 'line-number') {
const normalizeDecorationProperties = function (decoration, decorationParams) {
decorationParams.id = decoration.id
if (decorationParams.type === 'line-number' && decorationParams.gutterName == null) {
decorationParams.gutterName = 'line-number'
}
if (decorationParams.order == null) {
decorationParams.order = Infinity
}
return decorationParams
}
@@ -164,7 +169,7 @@ class Decoration {
setProperties (newProperties) {
if (this.destroyed) { return }
const oldProperties = this.properties
this.properties = translateDecorationParamsOldToNew(newProperties)
this.properties = normalizeDecorationProperties(this, newProperties)
if (newProperties.type != null) {
this.decorationManager.decorationDidChangeType(this)
}

View File

@@ -1,84 +0,0 @@
fs = require 'fs'
{Directory} = require 'pathwatcher'
GitRepository = require './git-repository'
# Returns the .gitdir path in the agnostic Git symlink .git file given, or
# null if the path is not a valid gitfile.
#
# * `gitFile` {String} path of gitfile to parse
gitFileRegex = RegExp "^gitdir: (.+)"
pathFromGitFile = (gitFile) ->
try
gitFileBuff = fs.readFileSync(gitFile, 'utf8')
return gitFileBuff?.match(gitFileRegex)[1]
# Checks whether a valid `.git` directory is contained within the given
# directory or one of its ancestors. If so, a Directory that corresponds to the
# `.git` folder will be returned. Otherwise, returns `null`.
#
# * `directory` {Directory} to explore whether it is part of a Git repository.
findGitDirectorySync = (directory) ->
# TODO: Fix node-pathwatcher/src/directory.coffee so the following methods
# can return cached values rather than always returning new objects:
# getParent(), getFile(), getSubdirectory().
gitDir = directory.getSubdirectory('.git')
gitDirPath = pathFromGitFile(gitDir.getPath?())
if gitDirPath
gitDir = new Directory(directory.resolve(gitDirPath))
if gitDir.existsSync?() and isValidGitDirectorySync gitDir
gitDir
else if directory.isRoot()
return null
else
findGitDirectorySync directory.getParent()
# Returns a boolean indicating whether the specified directory represents a Git
# repository.
#
# * `directory` {Directory} whose base name is `.git`.
isValidGitDirectorySync = (directory) ->
# To decide whether a directory has a valid .git folder, we use
# the heuristic adopted by the valid_repository_path() function defined in
# node_modules/git-utils/deps/libgit2/src/repository.c.
return directory.getSubdirectory('objects').existsSync() and
directory.getFile('HEAD').existsSync() and
directory.getSubdirectory('refs').existsSync()
# Provider that conforms to the atom.repository-provider@0.1.0 service.
module.exports =
class GitRepositoryProvider
constructor: (@project, @config) ->
# Keys are real paths that end in `.git`.
# Values are the corresponding GitRepository objects.
@pathToRepository = {}
# Returns a {Promise} that resolves with either:
# * {GitRepository} if the given directory has a Git repository.
# * `null` if the given directory does not have a Git repository.
repositoryForDirectory: (directory) ->
# TODO: Currently, this method is designed to be async, but it relies on a
# synchronous API. It should be rewritten to be truly async.
Promise.resolve(@repositoryForDirectorySync(directory))
# Returns either:
# * {GitRepository} if the given directory has a Git repository.
# * `null` if the given directory does not have a Git repository.
repositoryForDirectorySync: (directory) ->
# Only one GitRepository should be created for each .git folder. Therefore,
# we must check directory and its parent directories to find the nearest
# .git folder.
gitDir = findGitDirectorySync(directory)
unless gitDir
return null
gitDirPath = gitDir.getPath()
repo = @pathToRepository[gitDirPath]
unless repo
repo = GitRepository.open(gitDirPath, {@project, @config})
return null unless repo
repo.onDidDestroy(=> delete @pathToRepository[gitDirPath])
@pathToRepository[gitDirPath] = repo
repo.refreshIndex()
repo.refreshStatus()
repo

View File

@@ -0,0 +1,180 @@
const fs = require('fs')
const { Directory } = require('pathwatcher')
const GitRepository = require('./git-repository')
const GIT_FILE_REGEX = RegExp('^gitdir: (.+)')
// Returns the .gitdir path in the agnostic Git symlink .git file given, or
// null if the path is not a valid gitfile.
//
// * `gitFile` {String} path of gitfile to parse
function pathFromGitFileSync (gitFile) {
try {
const gitFileBuff = fs.readFileSync(gitFile, 'utf8')
return gitFileBuff != null ? gitFileBuff.match(GIT_FILE_REGEX)[1] : null
} catch (error) {}
}
// Returns a {Promise} that resolves to the .gitdir path in the agnostic
// Git symlink .git file given, or null if the path is not a valid gitfile.
//
// * `gitFile` {String} path of gitfile to parse
function pathFromGitFile (gitFile) {
return new Promise(resolve => {
fs.readFile(gitFile, 'utf8', (err, gitFileBuff) => {
if (err == null && gitFileBuff != null) {
const result = gitFileBuff.toString().match(GIT_FILE_REGEX)
resolve(result != null ? result[1] : null)
} else {
resolve(null)
}
})
})
}
// Checks whether a valid `.git` directory is contained within the given
// directory or one of its ancestors. If so, a Directory that corresponds to the
// `.git` folder will be returned. Otherwise, returns `null`.
//
// * `directory` {Directory} to explore whether it is part of a Git repository.
function findGitDirectorySync (directory) {
// TODO: Fix node-pathwatcher/src/directory.coffee so the following methods
// can return cached values rather than always returning new objects:
// getParent(), getFile(), getSubdirectory().
let gitDir = directory.getSubdirectory('.git')
if (typeof gitDir.getPath === 'function') {
const gitDirPath = pathFromGitFileSync(gitDir.getPath())
if (gitDirPath) {
gitDir = new Directory(directory.resolve(gitDirPath))
}
}
if (
typeof gitDir.existsSync === 'function' &&
gitDir.existsSync() &&
isValidGitDirectorySync(gitDir)
) {
return gitDir
} else if (directory.isRoot()) {
return null
} else {
return findGitDirectorySync(directory.getParent())
}
}
// Checks whether a valid `.git` directory is contained within the given
// directory or one of its ancestors. If so, a Directory that corresponds to the
// `.git` folder will be returned. Otherwise, returns `null`.
//
// Returns a {Promise} that resolves to
// * `directory` {Directory} to explore whether it is part of a Git repository.
async function findGitDirectory (directory) {
// TODO: Fix node-pathwatcher/src/directory.coffee so the following methods
// can return cached values rather than always returning new objects:
// getParent(), getFile(), getSubdirectory().
let gitDir = directory.getSubdirectory('.git')
if (typeof gitDir.getPath === 'function') {
const gitDirPath = await pathFromGitFile(gitDir.getPath())
if (gitDirPath) {
gitDir = new Directory(directory.resolve(gitDirPath))
}
}
if (
typeof gitDir.exists === 'function' &&
(await gitDir.exists()) &&
isValidGitDirectory(gitDir)
) {
return gitDir
} else if (directory.isRoot()) {
return null
} else {
return await findGitDirectory(directory.getParent())
}
}
// Returns a boolean indicating whether the specified directory represents a Git
// repository.
//
// * `directory` {Directory} whose base name is `.git`.
function isValidGitDirectorySync (directory) {
// To decide whether a directory has a valid .git folder, we use
// the heuristic adopted by the valid_repository_path() function defined in
// node_modules/git-utils/deps/libgit2/src/repository.c.
return (
directory.getSubdirectory('objects').existsSync() &&
directory.getFile('HEAD').existsSync() &&
directory.getSubdirectory('refs').existsSync()
)
}
// Returns a {Promise} that resolves to a {Boolean} indicating whether the
// specified directory represents a Git repository.
//
// * `directory` {Directory} whose base name is `.git`.
async function isValidGitDirectory (directory) {
// To decide whether a directory has a valid .git folder, we use
// the heuristic adopted by the valid_repository_path() function defined in
// node_modules/git-utils/deps/libgit2/src/repository.c.
return (
(await directory.getSubdirectory('objects').exists()) &&
(await directory.getFile('HEAD').exists()) &&
(await directory.getSubdirectory('refs').exists())
)
}
// Provider that conforms to the atom.repository-provider@0.1.0 service.
class GitRepositoryProvider {
constructor (project, config) {
// Keys are real paths that end in `.git`.
// Values are the corresponding GitRepository objects.
this.project = project
this.config = config
this.pathToRepository = {}
}
// Returns a {Promise} that resolves with either:
// * {GitRepository} if the given directory has a Git repository.
// * `null` if the given directory does not have a Git repository.
async repositoryForDirectory (directory) {
// Only one GitRepository should be created for each .git folder. Therefore,
// we must check directory and its parent directories to find the nearest
// .git folder.
const gitDir = await findGitDirectory(directory)
return this.repositoryForGitDirectory(gitDir)
}
// Returns either:
// * {GitRepository} if the given directory has a Git repository.
// * `null` if the given directory does not have a Git repository.
repositoryForDirectorySync (directory) {
// Only one GitRepository should be created for each .git folder. Therefore,
// we must check directory and its parent directories to find the nearest
// .git folder.
const gitDir = findGitDirectorySync(directory)
return this.repositoryForGitDirectory(gitDir)
}
// Returns either:
// * {GitRepository} if the given Git directory has a Git repository.
// * `null` if the given directory does not have a Git repository.
repositoryForGitDirectory (gitDir) {
if (!gitDir) {
return null
}
const gitDirPath = gitDir.getPath()
let repo = this.pathToRepository[gitDirPath]
if (!repo) {
repo = GitRepository.open(gitDirPath, { project: this.project, config: this.config })
if (!repo) {
return null
}
repo.onDidDestroy(() => delete this.pathToRepository[gitDirPath])
this.pathToRepository[gitDirPath] = repo
repo.refreshIndex()
repo.refreshStatus()
}
return repo
}
}
module.exports = GitRepositoryProvider

View File

@@ -215,8 +215,10 @@ class GrammarRegistry {
// If multiple grammars match by one of the above criteria, break ties.
if (score > 0) {
const isTreeSitter = grammar instanceof TreeSitterGrammar
// Prefer either TextMate or Tree-sitter grammars based on the user's settings.
if (grammar instanceof TreeSitterGrammar) {
if (isTreeSitter) {
if (this.shouldUseTreeSitterParser(grammar.scopeName)) {
score += 0.1
} else {
@@ -227,7 +229,8 @@ class GrammarRegistry {
// Prefer grammars with matching content regexes. Prefer a grammar with no content regex
// over one with a non-matching content regex.
if (grammar.contentRegex) {
if (grammar.contentRegex.test(contents)) {
const contentMatch = isTreeSitter ? grammar.contentRegex.test(contents) : grammar.contentRegex.testSync(contents)
if (contentMatch) {
score += 0.05
} else {
score -= 0.05

View File

@@ -32,7 +32,17 @@ module.exports = ({blobStore}) ->
{testRunnerPath, legacyTestRunnerPath, headless, logFile, testPaths, env} = getWindowLoadSettings()
unless headless
if headless
# Install console functions that output to stdout and stderr.
util = require 'util'
Object.defineProperties process,
stdout: {value: remote.process.stdout}
stderr: {value: remote.process.stderr}
console.log = (args...) -> process.stdout.write "#{util.format(args...)}\n"
console.error = (args...) -> process.stderr.write "#{util.format(args...)}\n"
else
# Show window synchronously so a focusout doesn't fire on input elements
# that are focused in the very first spec run.
remote.getCurrentWindow().show()

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

@@ -23,6 +23,97 @@ const ConfigSchema = require('../config-schema')
const LocationSuffixRegExp = /(:\d+)(:\d+)?$/
// Increment this when changing the serialization format of `${ATOM_HOME}/storage/application.json` used by
// AtomApplication::saveCurrentWindowOptions() and AtomApplication::loadPreviousWindowOptions() in a backward-
// incompatible way.
const APPLICATION_STATE_VERSION = '1'
const getDefaultPath = () => {
const editor = atom.workspace.getActiveTextEditor()
if (!editor || !editor.getPath()) {
return
}
const paths = atom.project.getPaths()
if (paths) {
return paths[0]
}
}
const getSocketSecretPath = (atomVersion) => {
const {username} = os.userInfo()
const atomHome = path.resolve(process.env.ATOM_HOME)
return path.join(atomHome, `.atom-socket-secret-${username}-${atomVersion}`)
}
const getSocketPath = (socketSecret) => {
if (!socketSecret) {
return null
}
// Hash the secret to create the socket name to not expose it.
const socketName = crypto
.createHmac('sha256', socketSecret)
.update('socketName')
.digest('hex')
.substr(0, 12)
if (process.platform === 'win32') {
return `\\\\.\\pipe\\atom-${socketName}-sock`
} else {
return path.join(os.tmpdir(), `atom-${socketName}.sock`)
}
}
const getExistingSocketSecret = (atomVersion) => {
const socketSecretPath = getSocketSecretPath(atomVersion)
if (!fs.existsSync(socketSecretPath)) {
return null
}
return fs.readFileSync(socketSecretPath, 'utf8')
}
const createSocketSecret = (atomVersion) => {
const socketSecret = crypto.randomBytes(16).toString('hex')
fs.writeFileSync(getSocketSecretPath(atomVersion), socketSecret, {encoding: 'utf8', mode: 0o600})
return socketSecret
}
const encryptOptions = (options, secret) => {
const message = JSON.stringify(options)
const initVector = crypto.randomBytes(16)
const cipher = crypto.createCipheriv('aes-256-gcm', secret, initVector)
let content = cipher.update(message, 'utf8', 'hex')
content += cipher.final('hex')
const authTag = cipher.getAuthTag().toString('hex')
return JSON.stringify({
authTag,
content,
initVector: initVector.toString('hex')
})
}
const decryptOptions = (optionsMessage, secret) => {
const {authTag, content, initVector} = JSON.parse(optionsMessage)
const decipher = crypto.createDecipheriv('aes-256-gcm', secret, Buffer.from(initVector, 'hex'))
decipher.setAuthTag(Buffer.from(authTag, 'hex'))
let message = decipher.update(content, 'hex', 'utf8')
message += decipher.final('utf8')
return JSON.parse(message)
}
// The application's singleton class.
//
// It's the entry point into the Atom application and maintains the global state
@@ -32,56 +123,36 @@ module.exports =
class AtomApplication extends EventEmitter {
// Public: The entry point into the Atom application.
static open (options) {
if (!options.socketPath) {
const {username} = os.userInfo()
// Lowercasing the ATOM_HOME to make sure that we don't get multiple sockets
// on case-insensitive filesystems due to arbitrary case differences in paths.
const atomHomeUnique = path.resolve(process.env.ATOM_HOME).toLowerCase()
const hash = crypto
.createHash('sha1')
.update(options.version)
.update('|')
.update(process.arch)
.update('|')
.update(username || '')
.update('|')
.update(atomHomeUnique)
// We only keep the first 12 characters of the hash as not to have excessively long
// socket file. Note that macOS/BSD limit the length of socket file paths (see #15081).
// The replace calls convert the digest into "URL and Filename Safe" encoding (see RFC 4648).
const atomInstanceDigest = hash
.digest('base64')
.substring(0, 12)
.replace(/\+/g, '-')
.replace(/\//g, '_')
if (process.platform === 'win32') {
options.socketPath = `\\\\.\\pipe\\atom-${atomInstanceDigest}-sock`
} else {
options.socketPath = path.join(os.tmpdir(), `atom-${atomInstanceDigest}.sock`)
}
}
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
// or electron, before it's fixed we check the existence of socketPath to
// speedup startup.
if ((process.platform !== 'win32' && !fs.existsSync(options.socketPath)) ||
options.test || options.benchmark || options.benchmarkTest) {
new AtomApplication(options).initialize(options)
return
if (
!socketPath || options.test || options.benchmark || options.benchmarkTest ||
(process.platform !== 'win32' && !fs.existsSync(socketPath))
) {
return createApplication(options)
}
const client = net.connect({path: options.socketPath}, () => {
client.write(JSON.stringify(options), () => {
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) {
@@ -91,6 +162,7 @@ class AtomApplication extends EventEmitter {
constructor (options) {
super()
this.quitting = false
this.quittingForUpdate = false
this.getAllWindows = this.getAllWindows.bind(this)
this.getLastFocusedWindow = this.getLastFocusedWindow.bind(this)
this.resourcePath = options.resourcePath
@@ -98,11 +170,14 @@ class AtomApplication extends EventEmitter {
this.version = options.version
this.devMode = options.devMode
this.safeMode = options.safeMode
this.socketPath = options.socketPath
this.logFile = options.logFile
this.userDataDir = options.userDataDir
this._killProcess = options.killProcess || process.kill.bind(process)
if (options.test || options.benchmark || options.benchmarkTest) this.socketPath = null
if (!options.test && !options.benchmark && !options.benchmarkTest) {
this.socketSecret = createSocketSecret(this.version)
this.socketPath = getSocketPath(this.socketSecret)
}
this.waitSessionsByWindow = new Map()
this.windowStack = new WindowStack()
@@ -177,39 +252,42 @@ class AtomApplication extends EventEmitter {
this.config.onDidChange('core.colorProfile', () => this.promptForRestart())
}
const optionsForWindowsToOpen = []
let optionsForWindowsToOpen = []
let shouldReopenPreviousWindows = false
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 = [...await this.loadPreviousWindowOptions(), ...optionsForWindowsToOpen]
}
if (optionsForWindowsToOpen.length === 0) {
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) {
const {
pathsToOpen,
executedFrom,
foldersToOpen,
urlsToOpen,
benchmark,
benchmarkTest,
@@ -217,15 +295,19 @@ class AtomApplication extends EventEmitter {
pidToKillWhenClosed,
devMode,
safeMode,
newWindow,
logFile,
profileStartup,
timeout,
clearWindowState,
addToLastWindow,
preserveFocus,
env
} = options
app.focus()
if (!preserveFocus) {
app.focus()
}
if (test) {
return this.runTests({
@@ -248,11 +330,13 @@ class AtomApplication extends EventEmitter {
timeout,
env
})
} else if (pathsToOpen.length > 0) {
} else if ((pathsToOpen && pathsToOpen.length > 0) || (foldersToOpen && foldersToOpen.length > 0)) {
return this.openPaths({
pathsToOpen,
foldersToOpen,
executedFrom,
pidToKillWhenClosed,
newWindow,
devMode,
safeMode,
profileStartup,
@@ -260,12 +344,16 @@ 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,
safeMode,
profileStartup,
@@ -276,6 +364,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)
@@ -311,6 +404,7 @@ class AtomApplication extends EventEmitter {
window.browserWindow.removeListener('blur', blurHandler)
})
window.browserWindow.webContents.once('did-finish-load', blurHandler)
this.saveCurrentWindowOptions(false)
}
}
@@ -334,11 +428,21 @@ class AtomApplication extends EventEmitter {
const server = net.createServer(connection => {
let data = ''
connection.on('data', chunk => { data += chunk })
connection.on('end', () => this.openWithOptions(JSON.parse(data)))
connection.on('end', () => {
try {
const options = decryptOptions(data, this.socketSecret)
this.openWithOptions(options)
} catch (e) {
// Error while parsing/decrypting the options passed by the client.
// We cannot trust the client, aborting.
}
})
})
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 () {
@@ -356,15 +460,37 @@ class AtomApplication extends EventEmitter {
}
}
deleteSocketSecretFile () {
if (!this.socketSecret) {
return
}
const socketSecretPath = getSocketSecretPath(this.version)
if (fs.existsSync(socketSecretPath)) {
try {
fs.unlinkSync(socketSecretPath)
} catch (error) {
// Ignore ENOENT errors in case the file was deleted between the exists
// check and the call to unlink sync.
if (error.code !== 'ENOENT') throw error
}
}
}
// Registers basic application commands, non-idempotent.
handleEvents () {
const getLoadSettings = () => {
const window = this.focusedWindow()
return {devMode: window && window.devMode, safeMode: window && window.safeMode}
const createOpenSettings = ({event, sameWindow}) => {
const targetWindow = event ? this.atomWindowForEvent(event) : this.focusedWindow()
return {
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()))
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}))
@@ -382,12 +508,27 @@ class AtomApplication extends EventEmitter {
this.on('application:install-update', () => {
this.quitting = true
this.quittingForUpdate = true
this.autoUpdateManager.install()
})
this.on('application:check-for-update', () => this.autoUpdateManager.check())
if (process.platform === 'darwin') {
this.on('application:reopen-project', ({ paths }) => {
this.openPaths({ pathsToOpen: paths })
})
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:'))
@@ -455,8 +596,12 @@ class AtomApplication extends EventEmitter {
this.disposable.add(ipcHelpers.on(app, 'will-quit', () => {
this.killAllProcesses()
this.deleteSocketFile()
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})
@@ -490,25 +635,36 @@ 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(new AtomWindow(this, this.fileRecoveryService, options))
this.addWindow(this.createWindow(options))
}
} else {
this.promptForPathToOpen('all', {window})
}
}))
// 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)
@@ -535,22 +691,9 @@ 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(), defaultPath)
case 'application:open-file':
return this.promptForPathToOpen('file', getLoadSettings(), defaultPath)
case 'application:open-folder':
return this.promptForPathToOpen('folder', getLoadSettings(), 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.emit(command, ...args)
return window && window.emit(command, ...args)
}))
this.disposable.add(ipcHelpers.respondTo('window-method', (browserWindow, method, ...args) => {
@@ -622,10 +765,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())
}
@@ -724,10 +863,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)
)
}
@@ -773,6 +914,7 @@ class AtomApplication extends EventEmitter {
// options -
// :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.
// :profileStartup - Boolean to control creating a profile of the startup time.
@@ -781,6 +923,7 @@ class AtomApplication extends EventEmitter {
openPath ({
pathToOpen,
pidToKillWhenClosed,
newWindow,
devMode,
safeMode,
profileStartup,
@@ -792,6 +935,7 @@ class AtomApplication extends EventEmitter {
return this.openPaths({
pathsToOpen: [pathToOpen],
pidToKillWhenClosed,
newWindow,
devMode,
safeMode,
profileStartup,
@@ -806,16 +950,20 @@ class AtomApplication extends EventEmitter {
//
// options -
// :pathsToOpen - The array of file paths to open
// :foldersToOpen - An array of additional paths to open that must be existing directories
// :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.
// :addToLastWindow - Boolean of whether this should be opened in last focused window.
openPaths ({
async openPaths ({
pathsToOpen,
foldersToOpen,
executedFrom,
pidToKillWhenClosed,
newWindow,
devMode,
safeMode,
windowDimensions,
@@ -825,27 +973,70 @@ class AtomApplication extends EventEmitter {
addToLastWindow,
env
} = {}) {
if (!pathsToOpen || pathsToOpen.length === 0) return
if (!env) env = process.env
if (!pathsToOpen) pathsToOpen = []
if (!foldersToOpen) foldersToOpen = []
devMode = Boolean(devMode)
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,
exists: true,
isDirectory: true,
hasWaitSession: pidToKillWhenClosed != null
})
})
const normalizedPathsToOpen = locationsToOpen.map(location => location.pathToOpen).filter(Boolean)
}
if (locationsToOpen.length === 0) {
return
}
const hasNonEmptyPath = locationsToOpen.some(location => location.pathToOpen)
const createNewWindow = newWindow || !hasNonEmptyPath
let existingWindow
if (addToLastWindow && normalizedPathsToOpen.length > 0) {
existingWindow = this.windowForPaths(normalizedPathsToOpen, devMode)
if (!createNewWindow) {
// An explicitly provided AtomWindow has precedence.
existingWindow = window
// 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, 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) {
let lastWindow = window || this.getLastFocusedWindow()
if (lastWindow && lastWindow.devMode === devMode) {
existingWindow = lastWindow
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
})
}
}
}
@@ -877,7 +1068,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,
@@ -898,7 +1089,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))
})
}
@@ -950,28 +1141,58 @@ class AtomApplication extends EventEmitter {
async saveCurrentWindowOptions (allowEmpty = false) {
if (this.quitting) return
const states = []
for (let window of this.getAllWindows()) {
if (!window.isSpec) states.push({initialPaths: window.representedDirectoryPaths})
const state = {
version: APPLICATION_STATE_VERSION,
windows: this.getAllWindows()
.filter(window => !window.isSpec)
.map(window => ({projectRoots: window.projectRoots}))
}
states.reverse()
state.windows.reverse()
if (states.length > 0 || allowEmpty) {
await this.storageFolder.store('application.json', states)
if (state.windows.length > 0 || allowEmpty) {
await this.storageFolder.store('application.json', state)
this.emit('application:did-save-state')
}
}
async loadPreviousWindowOptions () {
const states = await this.storageFolder.load('application.json')
if (states) {
return states.map(state => ({
pathsToOpen: state.initialPaths,
urlsToOpen: [],
const state = await this.storageFolder.load('application.json')
if (!state) {
return []
}
if (state.version === APPLICATION_STATE_VERSION) {
// Atom >=1.36.1
// Schema: {version: '1', windows: [{projectRoots: ['<root-dir>', ...]}, ...]}
return state.windows.map(each => ({
foldersToOpen: each.projectRoots,
devMode: this.devMode,
safeMode: this.safeMode
safeMode: this.safeMode,
newWindow: true
}))
} else if (state.version === undefined) {
// Atom <= 1.36.0
// Schema: [{initialPaths: ['<root-dir>', ...]}, ...]
return await Promise.all(
state.map(async windowState => {
// Classify each window's initialPaths as directories or non-directories
const classifiedPaths = await Promise.all(
windowState.initialPaths.map(initialPath => new Promise(resolve => {
fs.isDirectory(initialPath, isDir => resolve({initialPath, isDir}))
}))
)
// Only accept initialPaths that are existing directories
return {
foldersToOpen: classifiedPaths.filter(({isDir}) => isDir).map(({initialPath}) => initialPath),
devMode: this.devMode,
safeMode: this.safeMode,
newWindow: true
}
})
)
} else {
// Unrecognized version (from a newer Atom?)
return []
}
}
@@ -1018,6 +1239,7 @@ class AtomApplication extends EventEmitter {
if (bestWindow) {
bestWindow.sendURIMessage(url)
bestWindow.focus()
return bestWindow
} else {
let windowInitializationScript
let {resourcePath} = this
@@ -1035,7 +1257,7 @@ class AtomApplication extends EventEmitter {
}
const windowDimensions = this.getDimensionsForNewWindow()
const window = new AtomWindow(this, this.fileRecoveryService, {
const window = this.createWindow({
resourcePath,
windowInitializationScript,
devMode,
@@ -1059,7 +1281,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,
@@ -1141,7 +1363,7 @@ class AtomApplication extends EventEmitter {
if (safeMode == null) {
safeMode = false
}
const window = new AtomWindow(this, this.fileRecoveryService, {
const window = this.createWindow({
windowInitializationScript,
resourcePath,
headless,
@@ -1190,7 +1412,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,
@@ -1245,31 +1467,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.
@@ -1283,14 +1532,32 @@ class AtomApplication extends EventEmitter {
// should be in dev mode or not.
// :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.
// :window - An {AtomWindow} to use for opening selected file paths as long as
// all are files.
// :path - An optional String which controls the default path to which the
// file dialog opens.
promptForPathToOpen (type, {devMode, safeMode, window}, path = null) {
return this.promptForPath(
type,
pathsToOpen => {
return this.openPaths({pathsToOpen, devMode, safeMode, window})
async pathsToOpen => {
let targetWindow
// Open in :window as long as no chosen paths are folders. If any chosen path is a folder, open in a
// new window instead.
if (type === 'folder') {
targetWindow = null
} else if (type === 'file') {
targetWindow = window
} else if (type === 'all') {
const areDirectories = await Promise.all(
pathsToOpen.map(pathToOpen => new Promise(resolve => fs.isDirectory(pathToOpen, resolve)))
)
if (!areDirectories.some(Boolean)) {
targetWindow = window
}
}
return this.openPaths({pathsToOpen, devMode, safeMode, window: targetWindow})
},
path
)
@@ -1306,8 +1573,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 = {
@@ -1339,7 +1606,6 @@ class AtomApplication extends EventEmitter {
const args = []
if (this.safeMode) args.push('--safe')
if (this.logFile != null) args.push(`--log-file=${this.logFile}`)
if (this.socketPath != null) args.push(`--socket-path=${this.socketPath}`)
if (this.userDataDir != null) args.push(`--user-data-dir=${this.userDataDir}`)
if (this.devMode) {
args.push('--dev')

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,11 @@ 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.addLocationsToOpen(locationsToOpen)
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 +86,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 +122,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,24 +134,26 @@ 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
})
}
handleEvents () {
this.browserWindow.on('close', async event => {
if (!this.atomApplication.quitting && !this.unloading) {
if ((!this.atomApplication.quitting || this.atomApplication.quittingForUpdate) && !this.unloading) {
event.preventDefault()
this.unloading = true
this.atomApplication.saveCurrentWindowOptions(false)
@@ -232,6 +237,7 @@ class AtomWindow extends EventEmitter {
}
async openLocations (locationsToOpen) {
this.addLocationsToOpen(locationsToOpen)
await this.loadedPromise
this.sendMessage('open-locations', locationsToOpen)
}
@@ -244,6 +250,18 @@ class AtomWindow extends EventEmitter {
this.sendMessage('did-fail-to-read-user-settings', message)
}
addLocationsToOpen (locationsToOpen) {
const roots = new Set(this.projectRoots || [])
for (const {pathToOpen, isDirectory} of locationsToOpen) {
if (isDirectory) {
roots.add(pathToOpen)
}
}
this.projectRoots = Array.from(roots)
this.projectRoots.sort()
}
replaceEnvironment (env) {
this.browserWindow.webContents.send('environment', env)
}
@@ -376,7 +394,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 +426,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 +444,8 @@ class AtomWindow extends EventEmitter {
disableZoom () {
return this.browserWindow.webContents.setVisualZoomLevelLimits(1, 1)
}
getLoadedPromise () {
return this.loadedPromise
}
}

View File

@@ -39,7 +39,7 @@ class AutoUpdater extends EventEmitter {
}
supportsUpdates () {
SquirrelUpdate.existsSync()
return SquirrelUpdate.existsSync()
}
checkForUpdates () {

View File

@@ -15,16 +15,13 @@ module.exports = function parseCommandLine (processArgs) {
atom [options] [path ...]
atom file[:line[:column]]
If no arguments are given and no Atom windows are already open, restore all windows
from the previous editing session. Use "atom --new-window" to open a single empty
Atom window instead.
One or more paths to files or folders may be specified. If there is an
existing Atom window that contains all of the given folders, the paths
will be opened in that window. Otherwise, they will be opened in a new
window.
If no arguments are given and at least one Atom window is open, open a new, empty
Atom window.
One or more paths to files or folders may be specified. All paths will be opened
in a new Atom window. Each file may be opened at the desired line (and optionally
column) by appending the numbers after the file name, e.g. \`atom file:5:8\`.
A file may be opened at the desired line (and optionally column) by
appending the numbers right after the file name, e.g. \`atom file:5:8\`.
Paths that start with \`atom://\` will be interpreted as URLs.
@@ -43,7 +40,7 @@ module.exports = function parseCommandLine (processArgs) {
options.alias('f', 'foreground').boolean('f').describe('f', 'Keep the main process in the foreground.')
options.alias('h', 'help').boolean('h').describe('h', 'Print this usage message.')
options.alias('l', 'log-file').string('l').describe('l', 'Log all output to file.')
options.alias('n', 'new-window').boolean('n').describe('n', 'Launch an empty Atom window instead of restoring previous session.')
options.alias('n', 'new-window').boolean('n').describe('n', 'Open a new window.')
options.boolean('profile-startup').describe('profile-startup', 'Create a profile of the startup execution time.')
options.alias('r', 'resource-path').string('r').describe('r', 'Set the path to the Atom source directory and enable dev-mode.')
options.boolean('safe').describe(
@@ -61,7 +58,6 @@ module.exports = function parseCommandLine (processArgs) {
options.alias('v', 'version').boolean('v').describe('v', 'Print the version information.')
options.alias('w', 'wait').boolean('w').describe('w', 'Wait for window to be closed before returning.')
options.alias('a', 'add').boolean('a').describe('add', 'Open path as a new project in last used window.')
options.string('socket-path')
options.string('user-data-dir')
options.boolean('clear-window-state').describe('clear-window-state', 'Delete all Atom environment state.')
options.boolean('enable-electron-logging').describe('enable-electron-logging', 'Enable low-level logging messages from Electron.')
@@ -108,13 +104,22 @@ module.exports = function parseCommandLine (processArgs) {
executedFrom = process.cwd()
}
if (newWindow && addToLastWindow) {
process.stderr.write(
`Only one of the --add and --new-window options may be specified at the same time.\n\n${options.help()}`,
)
// Exiting the main process with a nonzero exit code on MacOS causes the app open to fail with the mysterious
// message "LSOpenURLsWithRole() failed for the application /Applications/Atom Dev.app with error -10810."
process.exit(0)
}
let pidToKillWhenClosed = null
if (args['wait']) {
pidToKillWhenClosed = args['pid']
}
const logFile = args['log-file']
const socketPath = args['socket-path']
const userDataDir = args['user-data-dir']
const profileStartup = args['profile-startup']
const clearWindowState = args['clear-window-state']
@@ -151,7 +156,6 @@ module.exports = function parseCommandLine (processArgs) {
safeMode,
newWindow,
logFile,
socketPath,
userDataDir,
profileStartup,
timeout,

View File

@@ -393,7 +393,6 @@ class Pane {
// Called by the view layer to indicate that the pane has gained focus.
focus () {
this.focused = true
return this.activate()
}
@@ -1011,6 +1010,8 @@ class Pane {
// Public: Makes this pane the *active* pane, causing it to gain focus.
activate () {
if (this.isDestroyed()) throw new Error('Pane has been destroyed')
this.focused = true
if (this.container) this.container.didActivatePane(this)
this.emitter.emit('did-activate')
}

View File

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

View File

@@ -125,7 +125,7 @@ class ReopenProjectMenuManager {
submenu: projects.map((project, index) => ({
label: this.createLabel(project),
command: 'application:reopen-project',
commandDetail: {index: index}
commandDetail: { index: index, paths: project.paths }
}))
}
]

View File

@@ -84,6 +84,8 @@ class Selection {
//
// * `bufferRange` The new {Range} to select.
// * `options` (optional) {Object} with the keys:
// * `reversed` {Boolean} indicating whether to set the selection in a
// reversed orientation.
// * `preserveFolds` if `true`, the fold settings are preserved after the
// selection moves.
// * `autoscroll` {Boolean} indicating whether to autoscroll to the new
@@ -770,7 +772,8 @@ class Selection {
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
toggleLineComments (options = {}) {
if (!this.ensureWritable('toggleLineComments', options)) return
this.editor.toggleLineCommentsForBufferRows(...(this.getBufferRowRange() || []))
let bufferRowRange = this.getBufferRowRange() || [null, null]
this.editor.toggleLineCommentsForBufferRows(...bufferRowRange, {correctSelection: true, selection: this})
}
// Public: Cuts the selection until the end of the screen line.

View File

@@ -1191,6 +1191,10 @@ class TextEditorComponent {
decorationsByScreenLine.set(screenLine.id, decorations)
}
decorations.push(decoration)
// Order block decorations by increasing values of their "order" property. Break ties with "id", which mirrors
// their creation sequence.
decorations.sort((a, b) => a.order !== b.order ? a.order - b.order : a.id - b.id)
}
addTextDecorationToRender (decoration, screenRange, marker) {
@@ -3862,15 +3866,24 @@ class LinesTileComponent {
if (blockDecorations) {
blockDecorations.forEach((newDecorations, screenLineId) => {
var oldDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLineId) : null
for (var i = 0; i < newDecorations.length; i++) {
var newDecoration = newDecorations[i]
if (oldDecorations && oldDecorations.includes(newDecoration)) continue
const oldDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLineId) : null
const lineNode = lineComponentsByScreenLineId.get(screenLineId).element
let lastAfter = lineNode
for (let i = 0; i < newDecorations.length; i++) {
const newDecoration = newDecorations[i]
const element = TextEditor.viewForItem(newDecoration.item)
if (oldDecorations && oldDecorations.includes(newDecoration)) {
if (newDecoration.position === 'after') {
lastAfter = element
}
continue
}
var element = TextEditor.viewForItem(newDecoration.item)
var lineNode = lineComponentsByScreenLineId.get(screenLineId).element
if (newDecoration.position === 'after') {
this.element.insertBefore(element, lineNode.nextSibling)
this.element.insertBefore(element, lastAfter.nextSibling)
lastAfter = element
} else {
this.element.insertBefore(element, lineNode)
}

View File

@@ -2221,14 +2221,17 @@ class TextEditor {
//
// The following are the supported decorations types:
//
// * __line__: Adds your CSS `class` to the line nodes within the range
// marked by the marker
// * __line-number__: Adds your CSS `class` to the line number nodes within the
// range marked by the marker
// * __highlight__: Adds a new highlight div to the editor surrounding the
// range marked by the marker. When the user selects text, the selection is
// visualized with a highlight decoration internally. The structure of this
// highlight will be
// * __line__: Adds the given CSS `class` to the lines overlapping the rows
// spanned by the marker.
// * __line-number__: Adds the given CSS `class` to the line numbers overlapping
// the rows spanned by the marker
// * __text__: Injects spans into all text overlapping the marked range, then adds
// the given `class` or `style` to these spans. Use this to manipulate the foreground
// color or styling of text in a range.
// * __highlight__: Creates an absolutely-positioned `.highlight` div to the editor
// containing nested divs that cover the marked region. For example, when the user
// selects text, the selection is implemented with a highlight decoration. The structure
// of this highlight will be:
// ```html
// <div class="highlight <your-class>">
// <!-- Will be one region for each row in the range. Spans 2 lines? There will be 2 regions. -->
@@ -2236,45 +2239,25 @@ class TextEditor {
// </div>
// ```
// * __overlay__: Positions the view associated with the given item at the head
// or tail of the given `DisplayMarker`.
// * __gutter__: A decoration that tracks a {DisplayMarker} in a {Gutter}. Gutter
// decorations are created by calling {Gutter::decorateMarker} on the
// desired `Gutter` instance.
// or tail of the given `DisplayMarker`, depending on the `position` property.
// * __gutter__: Tracks a {DisplayMarker} in a {Gutter}. Gutter decorations are created
// by calling {Gutter::decorateMarker} on the desired `Gutter` instance.
// * __block__: Positions the view associated with the given item before or
// after the row of the given `TextEditorMarker`.
// after the row of the given {DisplayMarker}, depending on the `position` property.
// Block decorations at the same screen row are ordered by their `order` property.
// * __cursor__: Render a cursor at the head of the {DisplayMarker}. If multiple cursor decorations
// are created for the same marker, their class strings and style objects are combined
// into a single cursor. This decoration type may be used to style existing cursors
// by passing in their markers or to render artificial cursors that don't actaully
// exist in the model by passing a marker that isn't associated with a real cursor.
//
// ## Arguments
//
// * `marker` A {DisplayMarker} you want this decoration to follow.
// * `decorationParams` An {Object} representing the decoration e.g.
// `{type: 'line-number', class: 'linter-error'}`
// * `type` There are several supported decoration types. The behavior of the
// types are as follows:
// * `line` Adds the given `class` to the lines overlapping the rows
// spanned by the `DisplayMarker`.
// * `line-number` Adds the given `class` to the line numbers overlapping
// the rows spanned by the `DisplayMarker`.
// * `text` Injects spans into all text overlapping the marked range,
// then adds the given `class` or `style` properties to these spans.
// Use this to manipulate the foreground color or styling of text in
// a given range.
// * `highlight` Creates an absolutely-positioned `.highlight` div
// containing nested divs to cover the marked region. For example, this
// is used to implement selections.
// * `overlay` Positions the view associated with the given item at the
// head or tail of the given `DisplayMarker`, depending on the `position`
// property.
// * `gutter` Tracks a {DisplayMarker} in a {Gutter}. Created by calling
// {Gutter::decorateMarker} on the desired `Gutter` instance.
// * `block` Positions the view associated with the given item before or
// after the row of the given `TextEditorMarker`, depending on the `position`
// property.
// * `cursor` Renders a cursor at the head of the given marker. If multiple
// decorations are created for the same marker, their class strings and
// style objects are combined into a single cursor. You can use this
// decoration type to style existing cursors by passing in their markers
// or render artificial cursors that don't actually exist in the model
// by passing a marker that isn't actually associated with a cursor.
// * `type` Determines the behavior and appearance of this {Decoration}. Supported decoration types
// and their uses are listed above.
// * `class` This CSS class will be applied to the decorated line number,
// line, text spans, highlight regions, cursors, or overlay.
// * `style` An {Object} containing CSS style properties to apply to the
@@ -2300,12 +2283,15 @@ class TextEditor {
// Controls where the view is positioned relative to the `TextEditorMarker`.
// Values can be `'head'` (the default) or `'tail'` for overlay decorations, and
// `'before'` (the default) or `'after'` for block decorations.
// * `order` (optional) Only applicable to decorations of type `block`. Controls
// where the view is positioned relative to other block decorations at the
// same screen row. If unspecified, block decorations render oldest to newest.
// * `avoidOverflow` (optional) Only applicable to decorations of type
// `overlay`. Determines whether the decoration adjusts its horizontal or
// vertical position to remain fully visible when it would otherwise
// overflow the editor. Defaults to `true`.
//
// Returns a {Decoration} object
// Returns the created {Decoration} object.
decorateMarker (marker, decorationParams) {
return this.decorationManager.decorateMarker(marker, decorationParams)
}
@@ -4770,7 +4756,7 @@ class TextEditor {
toggleLineCommentForBufferRow (row) { this.toggleLineCommentsForBufferRows(row, row) }
toggleLineCommentsForBufferRows (start, end) {
toggleLineCommentsForBufferRows (start, end, options = {}) {
const languageMode = this.buffer.getLanguageMode()
let {commentStartString, commentEndString} =
languageMode.commentStringsForPosition &&
@@ -4800,6 +4786,23 @@ class TextEditor {
const indentLength = this.buffer.lineForRow(start).match(/^\s*/)[0].length
this.buffer.insert([start, indentLength], commentStartString + ' ')
this.buffer.insert([end, this.buffer.lineLengthForRow(end)], ' ' + commentEndString)
// Prevent the cursor from selecting / passing the delimiters
// See https://github.com/atom/atom/pull/17519
if (options.correctSelection && options.selection) {
const endLineLength = this.buffer.lineLengthForRow(end)
const oldRange = options.selection.getBufferRange()
if (oldRange.isEmpty()) {
if (oldRange.start.column === endLineLength) {
const endCol = endLineLength - commentEndString.length - 1
options.selection.setBufferRange([[end, endCol], [end, endCol]], {autoscroll: false})
}
} else {
const startDelta = oldRange.start.column === indentLength ? [0, commentStartString.length + 1] : [0, 0]
const endDelta = oldRange.end.column === endLineLength ? [0, -commentEndString.length - 1] : [0, 0]
options.selection.setBufferRange(oldRange.translate(startDelta, endDelta), {autoscroll: false})
}
}
})
}
} else {

View File

@@ -51,7 +51,9 @@ function shouldGetEnvFromShell (env) {
return false
}
if (env.ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT || process.env.ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT) {
const disableSellingOut = env.ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT || process.env.ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT
if (disableSellingOut === 'true') {
return false
}