mirror of
https://github.com/atom/atom.git
synced 2026-04-28 03:01:47 -04:00
Merge branch 'master' into autoFocus-element
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
180
src/git-repository-provider.js
Normal file
180
src/git-repository-provider.js
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class AutoUpdater extends EventEmitter {
|
||||
}
|
||||
|
||||
supportsUpdates () {
|
||||
SquirrelUpdate.existsSync()
|
||||
return SquirrelUpdate.existsSync()
|
||||
}
|
||||
|
||||
checkForUpdates () {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 }
|
||||
}))
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user