Merge branch 'master' into sm-hidden-all

This commit is contained in:
simurai
2017-01-14 10:09:07 +09:00
140 changed files with 3437 additions and 1485 deletions

View File

@@ -112,6 +112,7 @@ class ApplicationDelegate
loadSettings = getWindowLoadSettings()
loadSettings['initialPaths'] = paths
setWindowLoadSettings(loadSettings)
ipcRenderer.send("did-change-paths")
setAutoHideWindowMenuBar: (autoHide) ->
ipcHelpers.call('window-method', 'setAutoHideMenuBar', autoHide)
@@ -244,6 +245,17 @@ class ApplicationDelegate
didCancelWindowUnload: ->
ipcRenderer.send('did-cancel-window-unload')
onDidChangeHistoryManager: (callback) ->
outerCallback = (event, message) ->
callback(event)
ipcRenderer.on('did-change-history-manager', outerCallback)
new Disposable ->
ipcRenderer.removeListener('did-change-history-manager', outerCallback)
didChangeHistoryManager: ->
ipcRenderer.send('did-change-history-manager')
openExternal: (url) ->
shell.openExternal(url)

View File

@@ -13,6 +13,7 @@ StateStore = require './state-store'
StorageFolder = require './storage-folder'
{getWindowLoadSettings} = require './window-load-settings-helpers'
registerDefaultCommands = require './register-default-commands'
{updateProcessEnv} = require './update-process-env'
DeserializerManager = require './deserializer-manager'
ViewRegistry = require './view-registry'
@@ -22,6 +23,8 @@ KeymapManager = require './keymap-extensions'
TooltipManager = require './tooltip-manager'
CommandRegistry = require './command-registry'
GrammarRegistry = require './grammar-registry'
{HistoryManager, HistoryProject} = require './history-manager'
ReopenProjectMenuManager = require './reopen-project-menu-manager'
StyleManager = require './style-manager'
PackageManager = require './package-manager'
ThemeManager = require './theme-manager'
@@ -94,6 +97,9 @@ class AtomEnvironment extends Model
# Public: A {GrammarRegistry} instance
grammars: null
# Public: A {HistoryManager} instance
history: null
# Public: A {PackageManager} instance
packages: null
@@ -226,15 +232,13 @@ class AtomEnvironment extends Model
@observeAutoHideMenuBar()
checkPortableHomeWritable = =>
responseChannel = "check-portable-home-writable-response"
ipcRenderer.on responseChannel, (event, response) ->
ipcRenderer.removeAllListeners(responseChannel)
@notifications.addWarning("#{response.message.replace(/([\\\.+\\-_#!])/g, '\\$1')}") if not response.writable
@disposables.add new Disposable -> ipcRenderer.removeAllListeners(responseChannel)
ipcRenderer.send('check-portable-home-writable', responseChannel)
@history = new HistoryManager({@project, @commands, localStorage})
# Keep instances of HistoryManager in sync
@history.onDidChangeProjects (e) =>
@applicationDelegate.didChangeHistoryManager() unless e.reloaded
@disposables.add @applicationDelegate.onDidChangeHistoryManager(=> @history.loadState())
checkPortableHomeWritable()
new ReopenProjectMenuManager({@menu, @commands, @history, @config, open: (paths) => @open(pathsToOpen: paths)})
attachSaveStateListeners: ->
saveState = _.debounce((=>
@@ -280,13 +284,13 @@ class AtomEnvironment extends Model
@workspace.addOpener (uri) =>
switch uri
when 'atom://.atom/stylesheet'
@workspace.open(@styles.getUserStyleSheetPath())
@workspace.openTextFile(@styles.getUserStyleSheetPath())
when 'atom://.atom/keymap'
@workspace.open(@keymaps.getUserKeymapPath())
@workspace.openTextFile(@keymaps.getUserKeymapPath())
when 'atom://.atom/config'
@workspace.open(@config.getUserConfigPath())
@workspace.openTextFile(@config.getUserConfigPath())
when 'atom://.atom/init-script'
@workspace.open(@getUserInitScriptPath())
@workspace.openTextFile(@getUserInitScriptPath())
registerDefaultTargetForKeymaps: ->
@keymaps.defaultTarget = @views.getView(@workspace)
@@ -643,7 +647,7 @@ class AtomEnvironment extends Model
restoreWindowDimensions: ->
unless @windowDimensions? and @isValidDimensions(@windowDimensions)
@windowDimensions = @getDefaultWindowDimensions()
@setWindowDimensions(@windowDimensions).then -> @windowDimensions
@setWindowDimensions(@windowDimensions).then => @windowDimensions
restoreWindowBackground: ->
if backgroundColor = window.localStorage.getItem('atom:window-background-color')
@@ -662,7 +666,11 @@ class AtomEnvironment extends Model
# Call this method when establishing a real application window.
startEditorWindow: ->
@unloaded = false
@loadState().then (state) =>
updateProcessEnvPromise = updateProcessEnv(@getLoadSettings().env)
updateProcessEnvPromise.then =>
@packages.triggerActivationHook('core:loaded-shell-environment')
loadStatePromise = @loadState().then (state) =>
@windowDimensions = state?.windowDimensions
@displayWindow().then =>
@commandInstaller.installAtomCommand false, (error) ->
@@ -706,6 +714,8 @@ class AtomEnvironment extends Model
@openInitialEmptyEditorIfNecessary()
Promise.all([loadStatePromise, updateProcessEnvPromise])
serialize: (options) ->
version: @constructor.version
project: @project.serialize(options)

62
src/atom-paths.js Normal file
View File

@@ -0,0 +1,62 @@
/** @babel */
const fs = require('fs-plus')
const path = require('path')
const hasWriteAccess = (dir) => {
const testFilePath = path.join(dir, 'write.test')
try {
fs.writeFileSync(testFilePath, new Date().toISOString(), { flag: 'w+' })
fs.unlinkSync(testFilePath)
return true
} catch (err) {
return false
}
}
const getAppDirectory = () => {
switch (process.platform) {
case 'darwin':
return path.join(process.execPath.substring(0, process.execPath.indexOf('.app')), '..')
case 'linux':
case 'win32':
return path.join(process.execPath, '..')
}
}
module.exports = {
setAtomHome: (homePath) => {
// When a read-writeable .atom folder exists above app use that
const portableHomePath = path.join(getAppDirectory(), '.atom')
if (fs.existsSync(portableHomePath)) {
if (hasWriteAccess(portableHomePath)) {
process.env.ATOM_HOME = portableHomePath
} else {
// A path exists so it was intended to be used but we didn't have rights, so warn.
console.log(`Insufficient permission to portable Atom home "${portableHomePath}".`)
}
}
// Check ATOM_HOME environment variable next
if (process.env.ATOM_HOME !== undefined) {
return
}
// Fall back to default .atom folder in users home folder
process.env.ATOM_HOME = path.join(homePath, '.atom')
},
setUserData: (app) => {
const electronUserDataPath = path.join(process.env.ATOM_HOME, 'electronUserData')
if (fs.existsSync(electronUserDataPath)) {
if (hasWriteAccess(electronUserDataPath)) {
app.setPath('userData', electronUserDataPath)
} else {
// A path exists so it was intended to be used but we didn't have rights, so warn.
console.log(`Insufficient permission to Electron user data "${electronUserDataPath}".`)
}
}
},
getAppDirectory: getAppDirectory
}

View File

@@ -1,48 +0,0 @@
BufferedProcess = require './buffered-process'
path = require 'path'
# Extended: Like {BufferedProcess}, but accepts a Node script as the command
# to run.
#
# This is necessary on Windows since it doesn't support shebang `#!` lines.
#
# ## Examples
#
# ```coffee
# {BufferedNodeProcess} = require 'atom'
# ```
module.exports =
class BufferedNodeProcess extends BufferedProcess
# Public: Runs the given Node script by spawning a new child process.
#
# * `options` An {Object} with the following keys:
# * `command` The {String} path to the JavaScript script to execute.
# * `args` The {Array} of arguments to pass to the script (optional).
# * `options` The options {Object} to pass to Node's `ChildProcess.spawn`
# method (optional).
# * `stdout` The callback {Function} that receives a single argument which
# contains the standard output from the command. The callback is
# called as data is received but it's buffered to ensure only
# complete lines are passed until the source stream closes. After
# the source stream has closed all remaining data is sent in a
# final call (optional).
# * `stderr` The callback {Function} that receives a single argument which
# contains the standard error output from the command. The
# callback is called as data is received but it's buffered to
# ensure only complete lines are passed until the source stream
# closes. After the source stream has closed all remaining data
# is sent in a final call (optional).
# * `exit` The callback {Function} which receives a single argument
# containing the exit status (optional).
constructor: ({command, args, options, stdout, stderr, exit}) ->
options ?= {}
options.env ?= Object.create(process.env)
options.env['ELECTRON_RUN_AS_NODE'] = 1
options.env['ELECTRON_NO_ATTACH_CONSOLE'] = 1
args = args?.slice() ? []
args.unshift(command)
args.unshift('--no-deprecation')
super({command: process.execPath, args, options, stdout, stderr, exit})

View File

@@ -0,0 +1,56 @@
/** @babel */
import BufferedProcess from './buffered-process'
// Extended: Like {BufferedProcess}, but accepts a Node script as the command
// to run.
//
// This is necessary on Windows since it doesn't support shebang `#!` lines.
//
// ## Examples
//
// ```js
// const {BufferedNodeProcess} = require('atom')
// ```
export default class BufferedNodeProcess extends BufferedProcess {
// Public: Runs the given Node script by spawning a new child process.
//
// * `options` An {Object} with the following keys:
// * `command` The {String} path to the JavaScript script to execute.
// * `args` The {Array} of arguments to pass to the script (optional).
// * `options` The options {Object} to pass to Node's `ChildProcess.spawn`
// method (optional).
// * `stdout` The callback {Function} that receives a single argument which
// contains the standard output from the command. The callback is
// called as data is received but it's buffered to ensure only
// complete lines are passed until the source stream closes. After
// the source stream has closed all remaining data is sent in a
// final call (optional).
// * `stderr` The callback {Function} that receives a single argument which
// contains the standard error output from the command. The
// callback is called as data is received but it's buffered to
// ensure only complete lines are passed until the source stream
// closes. After the source stream has closed all remaining data
// is sent in a final call (optional).
// * `exit` The callback {Function} which receives a single argument
// containing the exit status (optional).
constructor ({command, args, options = {}, stdout, stderr, exit}) {
options.env = options.env || Object.create(process.env)
options.env.ELECTRON_RUN_AS_NODE = 1
options.env.ELECTRON_NO_ATTACH_CONSOLE = 1
args = args ? args.slice() : []
args.unshift(command)
args.unshift('--no-deprecation')
super({
command: process.execPath,
args,
options,
stdout,
stderr,
exit
})
}
}

View File

@@ -1,246 +0,0 @@
_ = require 'underscore-plus'
ChildProcess = require 'child_process'
{Emitter} = require 'event-kit'
path = require 'path'
# Extended: A wrapper which provides standard error/output line buffering for
# Node's ChildProcess.
#
# ## Examples
#
# ```coffee
# {BufferedProcess} = require 'atom'
#
# command = 'ps'
# args = ['-ef']
# stdout = (output) -> console.log(output)
# exit = (code) -> console.log("ps -ef exited with #{code}")
# process = new BufferedProcess({command, args, stdout, exit})
# ```
module.exports =
class BufferedProcess
###
Section: Construction
###
# Public: Runs the given command by spawning a new child process.
#
# * `options` An {Object} with the following keys:
# * `command` The {String} command to execute.
# * `args` The {Array} of arguments to pass to the command (optional).
# * `options` {Object} (optional) The options {Object} to pass to Node's
# `ChildProcess.spawn` method.
# * `stdout` {Function} (optional) The callback that receives a single
# argument which contains the standard output from the command. The
# callback is called as data is received but it's buffered to ensure only
# complete lines are passed until the source stream closes. After the
# source stream has closed all remaining data is sent in a final call.
# * `data` {String}
# * `stderr` {Function} (optional) The callback that receives a single
# argument which contains the standard error output from the command. The
# callback is called as data is received but it's buffered to ensure only
# complete lines are passed until the source stream closes. After the
# source stream has closed all remaining data is sent in a final call.
# * `data` {String}
# * `exit` {Function} (optional) The callback which receives a single
# argument containing the exit status.
# * `code` {Number}
constructor: ({command, args, options, stdout, stderr, exit}={}) ->
@emitter = new Emitter
options ?= {}
@command = command
# Related to joyent/node#2318
if process.platform is 'win32' and not options.shell?
# Quote all arguments and escapes inner quotes
if args?
cmdArgs = args.filter (arg) -> arg?
cmdArgs = cmdArgs.map (arg) =>
if @isExplorerCommand(command) and /^\/[a-zA-Z]+,.*$/.test(arg)
# Don't wrap /root,C:\folder style arguments to explorer calls in
# quotes since they will not be interpreted correctly if they are
arg
else
"\"#{arg.toString().replace(/"/g, '\\"')}\""
else
cmdArgs = []
if /\s/.test(command)
cmdArgs.unshift("\"#{command}\"")
else
cmdArgs.unshift(command)
cmdArgs = ['/s', '/d', '/c', "\"#{cmdArgs.join(' ')}\""]
cmdOptions = _.clone(options)
cmdOptions.windowsVerbatimArguments = true
@spawn(@getCmdPath(), cmdArgs, cmdOptions)
else
@spawn(command, args, options)
@killed = false
@handleEvents(stdout, stderr, exit)
###
Section: Event Subscription
###
# Public: Will call your callback when an error will be raised by the process.
# Usually this is due to the command not being available or not on the PATH.
# You can call `handle()` on the object passed to your callback to indicate
# that you have handled this error.
#
# * `callback` {Function} callback
# * `errorObject` {Object}
# * `error` {Object} the error object
# * `handle` {Function} call this to indicate you have handled the error.
# The error will not be thrown if this function is called.
#
# Returns a {Disposable}
onWillThrowError: (callback) ->
@emitter.on 'will-throw-error', callback
###
Section: Helper Methods
###
# Helper method to pass data line by line.
#
# * `stream` The Stream to read from.
# * `onLines` The callback to call with each line of data.
# * `onDone` The callback to call when the stream has closed.
bufferStream: (stream, onLines, onDone) ->
stream.setEncoding('utf8')
buffered = ''
stream.on 'data', (data) =>
return if @killed
bufferedLength = buffered.length
buffered += data
lastNewlineIndex = data.lastIndexOf('\n')
if lastNewlineIndex isnt -1
lineLength = lastNewlineIndex + bufferedLength + 1
onLines(buffered.substring(0, lineLength))
buffered = buffered.substring(lineLength)
stream.on 'close', =>
return if @killed
onLines(buffered) if buffered.length > 0
onDone()
# Kill all child processes of the spawned cmd.exe process on Windows.
#
# This is required since killing the cmd.exe does not terminate child
# processes.
killOnWindows: ->
return unless @process?
parentPid = @process.pid
cmd = 'wmic'
args = [
'process'
'where'
"(ParentProcessId=#{parentPid})"
'get'
'processid'
]
try
wmicProcess = ChildProcess.spawn(cmd, args)
catch spawnError
@killProcess()
return
wmicProcess.on 'error', -> # ignore errors
output = ''
wmicProcess.stdout.on 'data', (data) -> output += data
wmicProcess.stdout.on 'close', =>
pidsToKill = output.split(/\s+/)
.filter (pid) -> /^\d+$/.test(pid)
.map (pid) -> parseInt(pid)
.filter (pid) -> pid isnt parentPid and 0 < pid < Infinity
for pid in pidsToKill
try
process.kill(pid)
@killProcess()
killProcess: ->
@process?.kill()
@process = null
isExplorerCommand: (command) ->
if command is 'explorer.exe' or command is 'explorer'
true
else if process.env.SystemRoot
command is path.join(process.env.SystemRoot, 'explorer.exe') or command is path.join(process.env.SystemRoot, 'explorer')
else
false
getCmdPath: ->
if process.env.comspec
process.env.comspec
else if process.env.SystemRoot
path.join(process.env.SystemRoot, 'System32', 'cmd.exe')
else
'cmd.exe'
# Public: Terminate the process.
kill: ->
return if @killed
@killed = true
if process.platform is 'win32'
@killOnWindows()
else
@killProcess()
undefined
spawn: (command, args, options) ->
try
@process = ChildProcess.spawn(command, args, options)
catch spawnError
process.nextTick => @handleError(spawnError)
handleEvents: (stdout, stderr, exit) ->
return unless @process?
stdoutClosed = true
stderrClosed = true
processExited = true
exitCode = 0
triggerExitCallback = ->
return if @killed
if stdoutClosed and stderrClosed and processExited
exit?(exitCode)
if stdout
stdoutClosed = false
@bufferStream @process.stdout, stdout, ->
stdoutClosed = true
triggerExitCallback()
if stderr
stderrClosed = false
@bufferStream @process.stderr, stderr, ->
stderrClosed = true
triggerExitCallback()
if exit
processExited = false
@process.on 'exit', (code) ->
exitCode = code
processExited = true
triggerExitCallback()
@process.on 'error', (error) => @handleError(error)
return
handleError: (error) ->
handled = false
handle = -> handled = true
@emitter.emit 'will-throw-error', {error, handle}
if error.code is 'ENOENT' and error.syscall.indexOf('spawn') is 0
error = new Error("Failed to spawn command `#{@command}`. Make sure `#{@command}` is installed and on your PATH", error.path)
error.name = 'BufferedProcessError'
throw error unless handled

298
src/buffered-process.js Normal file
View File

@@ -0,0 +1,298 @@
/** @babel */
import _ from 'underscore-plus'
import ChildProcess from 'child_process'
import {Emitter} from 'event-kit'
import path from 'path'
// Extended: A wrapper which provides standard error/output line buffering for
// Node's ChildProcess.
//
// ## Examples
//
// ```js
// {BufferedProcess} = require('atom')
//
// const command = 'ps'
// const args = ['-ef']
// const stdout = (output) => console.log(output)
// const exit = (code) => console.log("ps -ef exited with #{code}")
// const process = new BufferedProcess({command, args, stdout, exit})
// ```
export default class BufferedProcess {
/*
Section: Construction
*/
// Public: Runs the given command by spawning a new child process.
//
// * `options` An {Object} with the following keys:
// * `command` The {String} command to execute.
// * `args` The {Array} of arguments to pass to the command (optional).
// * `options` {Object} (optional) The options {Object} to pass to Node's
// `ChildProcess.spawn` method.
// * `stdout` {Function} (optional) The callback that receives a single
// argument which contains the standard output from the command. The
// callback is called as data is received but it's buffered to ensure only
// complete lines are passed until the source stream closes. After the
// source stream has closed all remaining data is sent in a final call.
// * `data` {String}
// * `stderr` {Function} (optional) The callback that receives a single
// argument which contains the standard error output from the command. The
// callback is called as data is received but it's buffered to ensure only
// complete lines are passed until the source stream closes. After the
// source stream has closed all remaining data is sent in a final call.
// * `data` {String}
// * `exit` {Function} (optional) The callback which receives a single
// argument containing the exit status.
// * `code` {Number}
constructor ({command, args, options = {}, stdout, stderr, exit} = {}) {
this.emitter = new Emitter()
this.command = command
// Related to joyent/node#2318
if (process.platform === 'win32' && options.shell === undefined) {
this.spawnWithEscapedWindowsArgs(command, args, options)
} else {
this.spawn(command, args, options)
}
this.killed = false
this.handleEvents(stdout, stderr, exit)
}
// Windows has a bunch of special rules that node still doesn't take care of for you
spawnWithEscapedWindowsArgs (command, args, options) {
let cmdArgs = []
// Quote all arguments and escapes inner quotes
if (args) {
cmdArgs = args.filter((arg) => arg != null)
.map((arg) => {
if (this.isExplorerCommand(command) && /^\/[a-zA-Z]+,.*$/.test(arg)) {
// Don't wrap /root,C:\folder style arguments to explorer calls in
// quotes since they will not be interpreted correctly if they are
return arg
} else {
// Escape double quotes by putting a backslash in front of them
return `\"${arg.toString().replace(/"/g, '\\"')}\"`
}
})
}
// The command itself is quoted if it contains spaces, &, ^, | or # chars
cmdArgs.unshift(/\s|&|\^|\(|\)|\||#/.test(command) ? `\"${command}\"` : command)
const cmdOptions = _.clone(options)
cmdOptions.windowsVerbatimArguments = true
this.spawn(this.getCmdPath(), ['/s', '/d', '/c', `\"${cmdArgs.join(' ')}\"`], cmdOptions)
}
/*
Section: Event Subscription
*/
// Public: Will call your callback when an error will be raised by the process.
// Usually this is due to the command not being available or not on the PATH.
// You can call `handle()` on the object passed to your callback to indicate
// that you have handled this error.
//
// * `callback` {Function} callback
// * `errorObject` {Object}
// * `error` {Object} the error object
// * `handle` {Function} call this to indicate you have handled the error.
// The error will not be thrown if this function is called.
//
// Returns a {Disposable}
onWillThrowError (callback) {
return this.emitter.on('will-throw-error', callback)
}
/*
Section: Helper Methods
*/
// Helper method to pass data line by line.
//
// * `stream` The Stream to read from.
// * `onLines` The callback to call with each line of data.
// * `onDone` The callback to call when the stream has closed.
bufferStream (stream, onLines, onDone) {
stream.setEncoding('utf8')
let buffered = ''
stream.on('data', (data) => {
if (this.killed) return
let bufferedLength = buffered.length
buffered += data
let lastNewlineIndex = data.lastIndexOf('\n')
if (lastNewlineIndex !== -1) {
let lineLength = lastNewlineIndex + bufferedLength + 1
onLines(buffered.substring(0, lineLength))
buffered = buffered.substring(lineLength)
}
})
stream.on('close', () => {
if (this.killed) return
if (buffered.length > 0) onLines(buffered)
onDone()
})
}
// Kill all child processes of the spawned cmd.exe process on Windows.
//
// This is required since killing the cmd.exe does not terminate child
// processes.
killOnWindows () {
if (!this.process) return
const parentPid = this.process.pid
const cmd = 'wmic'
const args = [
'process',
'where',
`(ParentProcessId=${parentPid})`,
'get',
'processid'
]
let wmicProcess
try {
wmicProcess = ChildProcess.spawn(cmd, args)
} catch (spawnError) {
this.killProcess()
return
}
wmicProcess.on('error', () => {}) // ignore errors
let output = ''
wmicProcess.stdout.on('data', (data) => {
output += data
})
wmicProcess.stdout.on('close', () => {
const pidsToKill = output.split(/\s+/)
.filter((pid) => /^\d+$/.test(pid))
.map((pid) => parseInt(pid))
.filter((pid) => pid !== parentPid && pid > 0 && pid < Infinity)
for (let pid of pidsToKill) {
try {
process.kill(pid)
} catch (error) {}
}
this.killProcess()
})
}
killProcess () {
if (this.process) this.process.kill()
this.process = null
}
isExplorerCommand (command) {
if (command === 'explorer.exe' || command === 'explorer') {
return true
} else if (process.env.SystemRoot) {
return command === path.join(process.env.SystemRoot, 'explorer.exe') || command === path.join(process.env.SystemRoot, 'explorer')
} else {
return false
}
}
getCmdPath () {
if (process.env.comspec) {
return process.env.comspec
} else if (process.env.SystemRoot) {
return path.join(process.env.SystemRoot, 'System32', 'cmd.exe')
} else {
return 'cmd.exe'
}
}
// Public: Terminate the process.
kill () {
if (this.killed) return
this.killed = true
if (process.platform === 'win32') {
this.killOnWindows()
} else {
this.killProcess()
}
}
spawn (command, args, options) {
try {
this.process = ChildProcess.spawn(command, args, options)
} catch (spawnError) {
process.nextTick(() => this.handleError(spawnError))
}
}
handleEvents (stdout, stderr, exit) {
if (!this.process) return
const triggerExitCallback = () => {
if (this.killed) return
if (stdoutClosed && stderrClosed && processExited && typeof exit === 'function') {
exit(exitCode)
}
}
let stdoutClosed = true
let stderrClosed = true
let processExited = true
let exitCode = 0
if (stdout) {
stdoutClosed = false
this.bufferStream(this.process.stdout, stdout, () => {
stdoutClosed = true
triggerExitCallback()
})
}
if (stderr) {
stderrClosed = false
this.bufferStream(this.process.stderr, stderr, () => {
stderrClosed = true
triggerExitCallback()
})
}
if (exit) {
processExited = false
this.process.on('exit', (code) => {
exitCode = code
processExited = true
triggerExitCallback()
})
}
this.process.on('error', (error) => {
this.handleError(error)
})
}
handleError (error) {
let handled = false
const handle = () => {
handled = true
}
this.emitter.emit('will-throw-error', {error, handle})
if (error.code === 'ENOENT' && error.syscall.indexOf('spawn') === 0) {
error = new Error(`Failed to spawn command \`${this.command}\`. Make sure \`${this.command}\` is installed and on your PATH`, error.path)
error.name = 'BufferedProcessError'
}
if (!handled) throw error
}
}

View File

@@ -7,12 +7,26 @@
var path = require('path')
var fs = require('fs-plus')
var PackageTranspilationRegistry = require('./package-transpilation-registry')
var CSON = null
var packageTranspilationRegistry = new PackageTranspilationRegistry()
var COMPILERS = {
'.js': require('./babel'),
'.ts': require('./typescript'),
'.coffee': require('./coffee-script')
'.js': packageTranspilationRegistry.wrapTranspiler(require('./babel')),
'.ts': packageTranspilationRegistry.wrapTranspiler(require('./typescript')),
'.coffee': packageTranspilationRegistry.wrapTranspiler(require('./coffee-script'))
}
exports.addTranspilerConfigForPath = function (packagePath, packageName, packageMeta, config) {
packagePath = fs.realpathSync(packagePath)
packageTranspilationRegistry.addTranspilerConfigForPath(packagePath, packageName, packageMeta, config)
}
exports.removeTranspilerConfigForPath = function (packagePath) {
packagePath = fs.realpathSync(packagePath)
packageTranspilationRegistry.removeTranspilerConfigForPath(packagePath)
}
var cacheStats = {}
@@ -118,6 +132,7 @@ require('source-map-support').install({
}
var compiler = COMPILERS[path.extname(filePath)]
if (!compiler) compiler = COMPILERS['.js']
try {
var fileData = readCachedJavascript(compiler.getCachePath(sourceCode, filePath))

View File

@@ -12,7 +12,7 @@ const configSchema = {
properties: {
ignoredNames: {
type: 'array',
default: ['.git', '.hg', '.svn', '.DS_Store', '._*', 'Thumbs.db'],
default: ['.git', '.hg', '.svn', '.DS_Store', '._*', 'Thumbs.db', 'desktop.ini'],
items: {
type: 'string'
},
@@ -85,6 +85,8 @@ const configSchema = {
default: 'utf8',
enum: [
'cp437',
'cp850',
'cp866',
'eucjp',
'euckr',
'gbk',
@@ -117,15 +119,24 @@ const configSchema = {
'windows1255',
'windows1256',
'windows1257',
'windows1258',
'windows866'
'windows1258'
]
},
openEmptyEditorOnStart: {
description: 'Automatically open an empty editor on startup.',
description: 'When checked opens an untitled editor when loading a blank environment (such as with _File > New Window_ or when "Restore Previous Windows On Start" is unchecked); otherwise no editor is opened when loading a blank environment. This setting has no effect when restoring a previous state.',
type: 'boolean',
default: true
},
restorePreviousWindowsOnStart: {
description: 'When checked restores the last state of all Atom windows when started from the icon or `atom` by itself from the command line; otherwise a blank environment is loaded.',
type: 'boolean',
default: true
},
reopenProjectMenuCount: {
description: 'How many recent projects to show in the Reopen Project menu.',
type: 'integer',
default: 15
},
automaticallyUpdate: {
description: 'Automatically update Atom when a new release is available.',
type: 'boolean',
@@ -159,7 +170,7 @@ const configSchema = {
warnOnLargeFileLimit: {
description: 'Warn before opening files larger than this number of megabytes.',
type: 'number',
default: 20
default: 40
}
}
},

View File

@@ -653,9 +653,6 @@ class Cursor extends Model
fn()
@autoscroll() if options.autoscroll ? @isLastCursor()
getPixelRect: ->
@editor.pixelRectForScreenRange(@getScreenRange())
getScreenRange: ->
{row, column} = @getScreenPosition()
new Range(new Point(row, column), new Point(row, column + 1))

View File

@@ -238,6 +238,7 @@ class GitRepository
# Public: Returns the git configuration value specified by the key.
#
# * `key` The {String} key for the configuration to lookup.
# * `path` An optional {String} path in the repository to get this information
# for, only needed if the repository has submodules.
getConfigValue: (key, path) -> @getRepo(path).getConfigValue(key)

View File

@@ -103,6 +103,7 @@ class GutterContainerComponent
@domNode.appendChild(gutterComponent.getDomNode())
else
@domNode.insertBefore(gutterComponent.getDomNode(), @domNode.children[indexInOldGutters])
indexInOldGutters += 1
# Remove any gutters that were not present in the new gutters state.
for gutterComponentDescription in @gutterComponents

150
src/history-manager.js Normal file
View File

@@ -0,0 +1,150 @@
/** @babel */
import {Emitter} from 'event-kit'
// Extended: History manager for remembering which projects have been opened.
//
// An instance of this class is always available as the `atom.history` global.
//
// The project history is used to enable the 'Reopen Project' menu.
export class HistoryManager {
constructor ({project, commands, localStorage}) {
this.localStorage = localStorage
commands.add('atom-workspace', {'application:clear-project-history': this.clearProjects.bind(this)})
this.emitter = new Emitter()
this.loadState()
project.onDidChangePaths((projectPaths) => this.addProject(projectPaths))
}
// Public: Obtain a list of previously opened projects.
//
// Returns an {Array} of {HistoryProject} objects, most recent first.
getProjects () {
return this.projects.map(p => new HistoryProject(p.paths, p.lastOpened))
}
// Public: Clear all projects from the history.
//
// Note: This is not a privacy function - other traces will still exist,
// e.g. window state.
clearProjects () {
this.projects = []
this.saveState()
this.didChangeProjects()
}
// Public: Invoke the given callback when the list of projects changes.
//
// * `callback` {Function}
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeProjects (callback) {
return this.emitter.on('did-change-projects', callback)
}
didChangeProjects (args) {
this.emitter.emit('did-change-projects', args || { reloaded: false })
}
addProject (paths, lastOpened) {
if (paths.length === 0) return
let project = this.getProject(paths)
if (!project) {
project = new HistoryProject(paths)
this.projects.push(project)
}
project.lastOpened = lastOpened || new Date()
this.projects.sort((a, b) => b.lastOpened - a.lastOpened)
this.saveState()
this.didChangeProjects()
}
getProject (paths) {
for (var i = 0; i < this.projects.length; i++) {
if (arrayEquivalent(paths, this.projects[i].paths)) {
return this.projects[i]
}
}
return null
}
loadState () {
const state = JSON.parse(this.localStorage.getItem('history'))
if (state && state.projects) {
this.projects = state.projects.filter(p => Array.isArray(p.paths) && p.paths.length > 0).map(p => new HistoryProject(p.paths, new Date(p.lastOpened)))
this.didChangeProjects({ reloaded: true })
} else {
this.projects = []
}
}
saveState () {
const state = JSON.stringify({
projects: this.projects.map(p => ({
paths: p.paths, lastOpened: p.lastOpened
}))
})
this.localStorage.setItem('history', state)
}
async importProjectHistory () {
for (let project of await HistoryImporter.getAllProjects()) {
this.addProject(project.paths, project.lastOpened)
}
this.saveState()
this.didChangeProjects()
}
}
function arrayEquivalent (a, b) {
if (a.length !== b.length) return false
for (var i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false
}
return true
}
export class HistoryProject {
constructor (paths, lastOpened) {
this.paths = paths
this.lastOpened = lastOpened || new Date()
}
set paths (paths) { this._paths = paths }
get paths () { return this._paths }
set lastOpened (lastOpened) { this._lastOpened = lastOpened }
get lastOpened () { return this._lastOpened }
}
class HistoryImporter {
static async getStateStoreCursor () {
const db = await atom.stateStore.dbPromise
const store = db.transaction(['states']).objectStore('states')
return store.openCursor()
}
static async getAllProjects (stateStore) {
const request = await HistoryImporter.getStateStoreCursor()
return new Promise((resolve, reject) => {
const rows = []
request.onerror = reject
request.onsuccess = event => {
const cursor = event.target.result
if (cursor) {
let project = cursor.value.value.project
let storedAt = cursor.value.storedAt
if (project && project.paths && storedAt) {
rows.push(new HistoryProject(project.paths, new Date(Date.parse(storedAt))))
}
cursor.continue()
} else {
resolve(rows)
}
}
})
}
}

View File

@@ -6,9 +6,7 @@ module.exports = ({blobStore}) ->
{getWindowLoadSettings} = require './window-load-settings-helpers'
{ipcRenderer} = require 'electron'
{resourcePath, devMode, env} = getWindowLoadSettings()
require '../src/electron-shims'
updateProcessEnv(env)
require './electron-shims'
# Add application-specific exports to module search path.
exportsPath = path.join(resourcePath, 'exports')

View File

@@ -13,6 +13,7 @@ export default async function () {
const ApplicationDelegate = require('../src/application-delegate')
const AtomEnvironment = require('../src/atom-environment')
const TextEditor = require('../src/text-editor')
require('./electron-shims')
const exportsPath = path.join(resourcePath, 'exports')
require('module').globalPaths.push(exportsPath) // Add 'exports' to module search path.

View File

@@ -19,11 +19,12 @@ module.exports = ({blobStore}) ->
path = require 'path'
{ipcRenderer} = require 'electron'
{getWindowLoadSettings} = require './window-load-settings-helpers'
CompileCache = require './compile-cache'
AtomEnvironment = require '../src/atom-environment'
ApplicationDelegate = require '../src/application-delegate'
Clipboard = require '../src/clipboard'
TextEditor = require '../src/text-editor'
require '../src/electron-shims'
require './electron-shims'
{testRunnerPath, legacyTestRunnerPath, headless, logFile, testPaths} = getWindowLoadSettings()
@@ -58,6 +59,13 @@ module.exports = ({blobStore}) ->
require('module').globalPaths.push(exportsPath)
process.env.NODE_PATH = exportsPath # Set NODE_PATH env variable since tasks may need it.
# Set up optional transpilation for packages under test if any
FindParentDir = require 'find-parent-dir'
if packageRoot = FindParentDir.sync(testPaths[0], 'package.json')
packageMetadata = require(path.join(packageRoot, 'package.json'))
if packageMetadata.atomTranspilers
CompileCache.addTranspilerConfigForPath(packageRoot, packageMetadata.name, packageMetadata, packageMetadata.atomTranspilers)
document.title = "Spec Suite"
clipboard = new Clipboard

View File

@@ -1,15 +1,6 @@
module.exports =
class InputComponent
constructor: ->
@domNode = document.createElement('input')
@domNode.classList.add('hidden-input')
@domNode.setAttribute('tabindex', -1)
@domNode.setAttribute('data-react-skip-selection-restoration', true)
@domNode.style['-webkit-transform'] = 'translateZ(0)'
@domNode.addEventListener 'paste', (event) -> event.preventDefault()
getDomNode: ->
@domNode
constructor: (@domNode) ->
updateSync: (state) ->
@oldState ?= {}

View File

@@ -8,6 +8,9 @@ bundledKeymaps = require('../package.json')?._atomKeymaps
KeymapManager::onDidLoadBundledKeymaps = (callback) ->
@emitter.on 'did-load-bundled-keymaps', callback
KeymapManager::onDidLoadUserKeymap = (callback) ->
@emitter.on 'did-load-user-keymap', callback
KeymapManager::loadBundledKeymaps = ->
keymapsPath = path.join(@resourcePath, 'keymaps')
if bundledKeymaps?
@@ -49,6 +52,9 @@ KeymapManager::loadUserKeymap = ->
stack = error.stack
@notificationManager.addFatalError(error.message, {detail, stack, dismissable: true})
@emitter.emit 'did-load-user-keymap'
KeymapManager::subscribeToFileReadFailure = ->
@onDidFailToReadFile (error) =>
userKeymapPath = @getUserKeymapPath()

View File

@@ -27,7 +27,7 @@ class LanguageMode
toggleLineCommentsForBufferRows: (start, end) ->
scope = @editor.scopeDescriptorForBufferPosition([start, 0])
commentStrings = @editor.getCommentStrings(scope)
return unless commentStrings?
return unless commentStrings?.commentStartString
{commentStartString, commentEndString} = commentStrings
buffer = @editor.buffer

View File

@@ -126,4 +126,8 @@ class LinesYardstick
clientRectForRange: (textNode, startIndex, endIndex) ->
@rangeForMeasurement.setStart(textNode, startIndex)
@rangeForMeasurement.setEnd(textNode, endIndex)
@rangeForMeasurement.getClientRects()[0] ? @rangeForMeasurement.getBoundingClientRect()
clientRects = @rangeForMeasurement.getClientRects()
if clientRects.length is 1
clientRects[0]
else
@rangeForMeasurement.getBoundingClientRect()

View File

@@ -142,8 +142,8 @@ class ApplicationMenu
item.metadata ?= {}
if item.command
item.accelerator = @acceleratorForCommand(item.command, keystrokesByCommand)
item.click = -> global.atomApplication.sendCommand(item.command)
item.metadata.windowSpecific = true unless /^application:/.test(item.command)
item.click = -> global.atomApplication.sendCommand(item.command, item.commandDetail)
item.metadata.windowSpecific = true unless /^application:/.test(item.command, item.commandDetail)
@translateTemplate(item.submenu, keystrokesByCommand) if item.submenu
template

View File

@@ -34,7 +34,7 @@ class AtomApplication
unless options.socketPath?
if process.platform is 'win32'
userNameSafe = new Buffer(process.env.USERNAME).toString('base64')
options.socketPath = "\\\\.\\pipe\\atom-#{options.version}-#{userNameSafe}-sock"
options.socketPath = "\\\\.\\pipe\\atom-#{options.version}-#{userNameSafe}-#{process.arch}-sock"
else
options.socketPath = path.join(os.tmpdir(), "atom-#{options.version}-#{process.env.USER}.sock")
@@ -63,7 +63,7 @@ class AtomApplication
exit: (status) -> app.exit(status)
constructor: (options) ->
{@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, @logFile, @setPortable, @userDataDir} = options
{@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, @logFile, @userDataDir} = options
@socketPath = null if options.test or options.benchmark or options.benchmarkTest
@pidsToOpenWindows = {}
@windows = []
@@ -99,7 +99,6 @@ class AtomApplication
@atomProtocolHandler = new AtomProtocolHandler(@resourcePath, @safeMode)
@listenForArgumentsFromNewProcess()
@setupJavaScriptArguments()
@setupDockMenu()
@launch(options)
@@ -207,10 +206,6 @@ class AtomApplication
# which is why this check is here.
throw error unless error.code is 'ENOENT'
# Configures required javascript environment flags.
setupJavaScriptArguments: ->
app.commandLine.appendSwitch 'js-flags', '--harmony'
# Registers basic application commands, non-idempotent.
handleEvents: ->
getLoadSettings = =>
@@ -285,6 +280,12 @@ class AtomApplication
@disposable.add ipcHelpers.on ipcMain, 'restart-application', =>
@restart()
@disposable.add ipcHelpers.on ipcMain, 'did-change-history-manager', (event) =>
for atomWindow in @windows
webContents = atomWindow.browserWindow.webContents
if webContents isnt event.sender
webContents.send('did-change-history-manager')
# A request from the associated render process to open a new render process.
@disposable.add ipcHelpers.on ipcMain, 'open', (event, options) =>
window = @atomWindowForEvent(event)
@@ -390,6 +391,9 @@ class AtomApplication
@fileRecoveryService.didSavePath(@atomWindowForEvent(event), path)
event.returnValue = true
@disposable.add ipcHelpers.on ipcMain, 'did-change-paths', =>
@saveState(false)
setupDockMenu: ->
if process.platform is 'darwin'
dockMenu = Menu.buildFromTemplate [
@@ -514,7 +518,7 @@ class AtomApplication
openPaths: ({initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window, clearWindowState, addToLastWindow, env}={}) ->
if not pathsToOpen? or pathsToOpen.length is 0
return
env = process.env unless env?
devMode = Boolean(devMode)
safeMode = Boolean(safeMode)
clearWindowState = Boolean(clearWindowState)
@@ -801,7 +805,6 @@ class AtomApplication
restart: ->
args = []
args.push("--safe") if @safeMode
args.push("--portable") if @setPortable
args.push("--log-file=#{@logFile}") if @logFile?
args.push("--socket-path=#{@socketPath}") if @socketPath?
args.push("--user-data-dir=#{@userDataDir}") if @userDataDir?

View File

@@ -1,58 +0,0 @@
const fs = require('fs-plus')
const path = require('path')
const {ipcMain} = require('electron')
module.exports = class AtomPortable {
static getPortableAtomHomePath () {
const execDirectoryPath = path.dirname(process.execPath)
return path.join(execDirectoryPath, '..', '.atom')
}
static setPortable (existingAtomHome) {
fs.copySync(existingAtomHome, this.getPortableAtomHomePath())
}
static isPortableInstall (platform, environmentAtomHome, defaultHome) {
if (!['linux', 'win32'].includes(platform)) {
return false
}
if (environmentAtomHome) {
return false
}
if (!fs.existsSync(this.getPortableAtomHomePath())) {
return false
}
// Currently checking only that the directory exists and is writable,
// probably want to do some integrity checks on contents in future.
return this.isPortableAtomHomePathWritable(defaultHome)
}
static isPortableAtomHomePathWritable (defaultHome) {
let writable = false
let message = ''
try {
const writePermissionTestFile = path.join(this.getPortableAtomHomePath(), 'write.test')
if (!fs.existsSync(writePermissionTestFile)) {
fs.writeFileSync(writePermissionTestFile, 'test')
}
fs.removeSync(writePermissionTestFile)
writable = true
} catch (error) {
message = `Failed to use portable Atom home directory (${this.getPortableAtomHomePath()}). Using the default instead (${defaultHome}). ${error.message}.`
}
ipcMain.on('check-portable-home-writable', function (event) {
event.sender.send('check-portable-home-writable-response', {
writable: writable,
message: message
})
})
return writable
}
}

View File

@@ -153,7 +153,10 @@ class AtomWindow
@browserWindow.destroy() if chosen is 0
@browserWindow.webContents.on 'crashed', =>
@atomApplication.exit(100) if @headless
if @headless
console.log "Renderer process crashed, exiting"
@atomApplication.exit(100)
return
@fileRecoveryService.didCrashWindow(this)
chosen = dialog.showMessageBox @browserWindow,

View File

@@ -17,13 +17,15 @@ class AutoUpdateManager
constructor: (@version, @testMode, resourcePath, @config) ->
@state = IdleState
@iconPath = path.resolve(__dirname, '..', '..', 'resources', 'atom.png')
@feedUrl = "https://atom.io/api/updates?version=#{@version}"
process.nextTick => @setupAutoUpdater()
setupAutoUpdater: ->
if process.platform is 'win32'
archSuffix = if process.arch is 'ia32' then '' else '-' + process.arch
@feedUrl = "https://atom.io/api/updates#{archSuffix}"
autoUpdater = require './auto-updater-win32'
else
@feedUrl = "https://atom.io/api/updates?version=#{@version}"
{autoUpdater} = require 'electron'
autoUpdater.on 'error', (event, message) =>

View File

@@ -41,10 +41,6 @@ module.exports = function parseCommandLine (processArgs) {
'safe',
'Do not load packages from ~/.atom/packages or ~/.atom/dev/packages.'
)
options.boolean('portable').describe(
'portable',
'Set portable mode. Copies the ~/.atom folder to be a sibling of the installed Atom location if a .atom folder is not already there.'
)
options.boolean('benchmark').describe('benchmark', 'Open a new window that runs the specified benchmarks.')
options.boolean('benchmark-test').describe('benchmark--test', 'Run a faster version of the benchmarks in headless mode.')
options.alias('t', 'test').boolean('t').describe('t', 'Run the specified specs and exit with error code on failures.')
@@ -104,7 +100,6 @@ module.exports = function parseCommandLine (processArgs) {
const profileStartup = args['profile-startup']
const clearWindowState = args['clear-window-state']
const urlsToOpen = []
const setPortable = args.portable
let devMode = args['dev']
let devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH || path.join(app.getPath('home'), 'github', 'atom')
let resourcePath = null
@@ -152,7 +147,6 @@ module.exports = function parseCommandLine (processArgs) {
userDataDir,
profileStartup,
timeout,
setPortable,
clearWindowState,
addToLastWindow,
mainProcess,

View File

@@ -1,10 +1,10 @@
const {app} = require('electron')
const fs = require('fs-plus')
const nslog = require('nslog')
const path = require('path')
const temp = require('temp')
const parseCommandLine = require('./parse-command-line')
const startCrashReporter = require('../crash-reporter-start')
const atomPaths = require('../atom-paths')
module.exports = function start (resourcePath, startTime) {
global.shellStartTime = startTime
@@ -23,7 +23,8 @@ module.exports = function start (resourcePath, startTime) {
console.log = nslog
const args = parseCommandLine(process.argv.slice(1))
setupAtomHome(args)
atomPaths.setAtomHome(app.getPath('home'))
atomPaths.setUserData()
setupCompileCache()
if (handleStartupEventWithSquirrel()) {
@@ -39,7 +40,7 @@ module.exports = function start (resourcePath, startTime) {
}
// NB: This prevents Win10 from showing dupe items in the taskbar
app.setAppUserModelId('com.squirrel.atom.atom')
app.setAppUserModelId('com.squirrel.atom.' + process.arch)
function addPathToOpen (event, pathToOpen) {
event.preventDefault()
@@ -79,36 +80,6 @@ function handleStartupEventWithSquirrel () {
return SquirrelUpdate.handleStartupEvent(app, squirrelCommand)
}
function setupAtomHome ({setPortable}) {
if (process.env.ATOM_HOME) {
return
}
let atomHome = path.join(app.getPath('home'), '.atom')
const AtomPortable = require('./atom-portable')
if (setPortable && !AtomPortable.isPortableInstall(process.platform, process.env.ATOM_HOME, atomHome)) {
try {
AtomPortable.setPortable(atomHome)
} catch (error) {
console.log(`Failed copying portable directory '${atomHome}' to '${AtomPortable.getPortableAtomHomePath()}'`)
console.log(`${error.message} ${error.stack}`)
}
}
if (AtomPortable.isPortableInstall(process.platform, process.env.ATOM_HOME, atomHome)) {
atomHome = AtomPortable.getPortableAtomHomePath()
}
try {
atomHome = fs.realpathSync(atomHome)
} catch (e) {
// Don't throw an error if atomHome doesn't exist.
}
process.env.ATOM_HOME = atomHome
}
function setupCompileCache () {
const CompileCache = require('../compile-cache')
CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME)

View File

@@ -1,63 +0,0 @@
Registry = require 'winreg'
Path = require 'path'
exeName = Path.basename(process.execPath)
appPath = "\"#{process.execPath}\""
fileIconPath = "\"#{Path.join(process.execPath, '..', 'resources', 'cli', 'file.ico')}\""
isBeta = appPath.includes(' Beta')
appName = exeName.replace('atom', (if isBeta then 'Atom Beta' else 'Atom' )).replace('.exe', '')
class ShellOption
constructor: (key, parts) ->
@key = key
@parts = parts
isRegistered: (callback) =>
new Registry({hive: 'HKCU', key: "#{@key}\\#{@parts[0].key}"})
.get @parts[0].name, (err, val) =>
callback(not err? and val? and val.value is @parts[0].value)
register: (callback) =>
doneCount = @parts.length
@parts.forEach (part) =>
reg = new Registry({hive: 'HKCU', key: if part.key? then "#{@key}\\#{part.key}" else @key})
reg.create( -> reg.set part.name, Registry.REG_SZ, part.value, -> callback() if --doneCount is 0)
deregister: (callback) =>
@isRegistered (isRegistered) =>
if isRegistered
new Registry({hive: 'HKCU', key: @key}).destroy -> callback null, true
else
callback null, false
update: (callback) =>
new Registry({hive: 'HKCU', key: "#{@key}\\#{@parts[0].key}"})
.get @parts[0].name, (err, val) =>
if err? or not val?
callback(err)
else
@register callback
exports.appName = appName
exports.fileHandler = new ShellOption("\\Software\\Classes\\Applications\\#{exeName}",
[
{key: 'shell\\open\\command', name: '', value: "#{appPath} \"%1\""},
{key: 'shell\\open', name: 'FriendlyAppName', value: "#{appName}"},
{key: 'DefaultIcon', name: '', value: "#{fileIconPath}"}
]
)
contextParts = [
{key: 'command', name: '', value: "#{appPath} \"%1\""},
{name: '', value: "Open with #{appName}"},
{name: 'Icon', value: "#{appPath}"}
]
exports.fileContextMenu = new ShellOption("\\Software\\Classes\\*\\shell\\#{appName}", contextParts)
exports.folderContextMenu = new ShellOption("\\Software\\Classes\\Directory\\shell\\#{appName}", contextParts)
exports.folderBackgroundContextMenu = new ShellOption("\\Software\\Classes\\Directory\\background\\shell\\#{appName}",
JSON.parse(JSON.stringify(contextParts).replace('%1', '%V'))
)

View File

@@ -0,0 +1,77 @@
'use babel'
import Registry from 'winreg'
import Path from 'path'
let exeName = Path.basename(process.execPath)
let appPath = `\"${process.execPath}\"`
let fileIconPath = `\"${Path.join(process.execPath, '..', 'resources', 'cli', 'file.ico')}\"`
let isBeta = appPath.includes(' Beta')
let appName = exeName.replace('atom', isBeta ? 'Atom Beta' : 'Atom').replace('.exe', '')
class ShellOption {
constructor (key, parts) {
this.isRegistered = this.isRegistered.bind(this)
this.register = this.register.bind(this)
this.deregister = this.deregister.bind(this)
this.update = this.update.bind(this)
this.key = key
this.parts = parts
}
isRegistered (callback) {
new Registry({hive: 'HKCU', key: `${this.key}\\${this.parts[0].key}`})
.get(this.parts[0].name, (err, val) => callback((err == null) && (val != null) && val.value === this.parts[0].value))
}
register (callback) {
let doneCount = this.parts.length
this.parts.forEach(part => {
let reg = new Registry({hive: 'HKCU', key: (part.key != null) ? `${this.key}\\${part.key}` : this.key})
return reg.create(() => reg.set(part.name, Registry.REG_SZ, part.value, () => { if (--doneCount === 0) return callback() }))
})
}
deregister (callback) {
this.isRegistered(isRegistered => {
if (isRegistered) {
new Registry({hive: 'HKCU', key: this.key}).destroy(() => callback(null, true))
} else {
callback(null, false)
}
})
}
update (callback) {
new Registry({hive: 'HKCU', key: `${this.key}\\${this.parts[0].key}`})
.get(this.parts[0].name, (err, val) => {
if ((err != null) || (val == null)) {
callback(err)
} else {
this.register(callback)
}
})
}
}
exports.appName = appName
exports.fileHandler = new ShellOption(`\\Software\\Classes\\Applications\\${exeName}`,
[
{key: 'shell\\open\\command', name: '', value: `${appPath} \"%1\"`},
{key: 'shell\\open', name: 'FriendlyAppName', value: `${appName}`},
{key: 'DefaultIcon', name: '', value: `${fileIconPath}`}
]
)
let contextParts = [
{key: 'command', name: '', value: `${appPath} \"%1\"`},
{name: '', value: `Open with ${appName}`},
{name: 'Icon', value: `${appPath}`}
]
exports.fileContextMenu = new ShellOption(`\\Software\\Classes\\*\\shell\\${appName}`, contextParts)
exports.folderContextMenu = new ShellOption(`\\Software\\Classes\\Directory\\shell\\${appName}`, contextParts)
exports.folderBackgroundContextMenu = new ShellOption(`\\Software\\Classes\\Directory\\background\\shell\\${appName}`,
JSON.parse(JSON.stringify(contextParts).replace('%1', '%V'))
)

View File

@@ -74,7 +74,13 @@ class NativeCompileCache {
self.cacheStore.delete(cacheKey)
}
} else {
let compilationResult = cachedVm.runInThisContext(wrapper, filename)
let compilationResult
try {
compilationResult = cachedVm.runInThisContext(wrapper, filename)
} catch (err) {
console.error(`Error running script ${filename}`)
throw err
}
if (compilationResult.cacheBuffer) {
self.cacheStore.set(cacheKey, invalidationKey, compilationResult.cacheBuffer)
}

View File

@@ -39,6 +39,7 @@ class PackageManager
@activationHookEmitter = new Emitter
@packageDirPaths = []
@deferredActivationHooks = []
@triggeredActivationHooks = new Set()
if configDirPath? and not safeMode
if @devMode
@packageDirPaths.push(path.join(configDirPath, "dev", "packages"))
@@ -67,6 +68,7 @@ class PackageManager
@deactivatePackages()
@loadedPackages = {}
@packageStates = {}
@triggeredActivationHooks.clear()
###
Section: Event Subscription
@@ -460,12 +462,17 @@ class PackageManager
Promise.resolve(pack)
else if pack = @loadPackage(name)
@activatingPackages[pack.name] = pack
pack.activate().then =>
activationPromise = pack.activate().then =>
if @activatingPackages[pack.name]?
delete @activatingPackages[pack.name]
@activePackages[pack.name] = pack
@emitter.emit 'did-activate-package', pack
pack
unless @deferredActivationHooks?
@triggeredActivationHooks.forEach((hook) => @activationHookEmitter.emit(hook))
activationPromise
else
Promise.reject(new Error("Failed to load package '#{name}'"))
@@ -476,6 +483,7 @@ class PackageManager
triggerActivationHook: (hook) ->
return new Error("Cannot trigger an empty activation hook") unless hook? and _.isString(hook) and hook.length > 0
@triggeredActivationHooks.add(hook)
if @deferredActivationHooks?
@deferredActivationHooks.push hook
else

View File

@@ -0,0 +1,172 @@
'use strict'
// This file is required by compile-cache, which is required directly from
// apm, so it can only use the subset of newer JavaScript features that apm's
// version of Node supports. Strict mode is required for block scoped declarations.
const crypto = require('crypto')
const fs = require('fs')
const path = require('path')
const minimatch = require('minimatch')
let Resolve = null
class PackageTranspilationRegistry {
constructor () {
this.configByPackagePath = {}
this.specByFilePath = {}
this.transpilerPaths = {}
}
addTranspilerConfigForPath (packagePath, packageName, packageMeta, config) {
this.configByPackagePath[packagePath] = {
name: packageName,
meta: packageMeta,
path: packagePath,
specs: config.map(spec => Object.assign({}, spec))
}
}
removeTranspilerConfigForPath (packagePath) {
delete this.configByPackagePath[packagePath]
}
// Wraps the transpiler in an object with the same interface
// that falls back to the original transpiler implementation if and
// only if a package hasn't registered its desire to transpile its own source.
wrapTranspiler (transpiler) {
return {
getCachePath: (sourceCode, filePath) => {
const spec = this.getPackageTranspilerSpecForFilePath(filePath)
if (spec) {
return this.getCachePath(sourceCode, filePath, spec)
}
return transpiler.getCachePath(sourceCode, filePath)
},
compile: (sourceCode, filePath) => {
const spec = this.getPackageTranspilerSpecForFilePath(filePath)
if (spec) {
return this.transpileWithPackageTranspiler(sourceCode, filePath, spec)
}
return transpiler.compile(sourceCode, filePath)
},
shouldCompile: (sourceCode, filePath) => {
if (this.transpilerPaths[filePath]) {
return false
}
const spec = this.getPackageTranspilerSpecForFilePath(filePath)
if (spec) {
return true
}
return transpiler.shouldCompile(sourceCode, filePath)
}
}
}
getPackageTranspilerSpecForFilePath (filePath) {
if (this.specByFilePath[filePath] !== undefined) return this.specByFilePath[filePath]
// ignore node_modules
if (filePath.indexOf(path.sep + 'node_modules' + path.sep) > -1) {
return false
}
let thisPath = filePath
let lastPath = null
// Iterate parents from the file path to the root, checking at each level
// to see if a package manages transpilation for that directory.
// This means searching for a config for `/path/to/file/here.js` only
// only iterates four times, even if there are hundreds of configs registered.
while (thisPath !== lastPath) { // until we reach the root
let config = this.configByPackagePath[thisPath]
if (config) {
for (let i = 0; i < config.specs.length; i++) {
const spec = config.specs[i]
if (minimatch(filePath, path.join(config.path, spec.glob))) {
spec._config = config
this.specByFilePath[filePath] = spec
return spec
}
}
}
lastPath = thisPath
thisPath = path.join(thisPath, '..')
}
this.specByFilePath[filePath] = null
return null
}
getCachePath (sourceCode, filePath, spec) {
const transpilerPath = this.getTranspilerPath(spec)
const transpilerSource = spec._transpilerSource || fs.readFileSync(transpilerPath, 'utf8')
spec._transpilerSource = transpilerSource
const transpiler = this.getTranspiler(spec)
let hash = crypto
.createHash('sha1')
.update(JSON.stringify(spec.options || {}))
.update(transpilerSource, 'utf8')
.update(sourceCode, 'utf8')
if (transpiler && transpiler.getCacheKeyData) {
const meta = this.getMetadata(spec)
const additionalCacheData = transpiler.getCacheKeyData(sourceCode, filePath, spec.options || {}, meta)
hash.update(additionalCacheData, 'utf8')
}
return path.join('package-transpile', spec._config.name, hash.digest('hex'))
}
transpileWithPackageTranspiler (sourceCode, filePath, spec) {
const transpiler = this.getTranspiler(spec)
if (transpiler) {
const meta = this.getMetadata(spec)
const result = transpiler.transpile(sourceCode, filePath, spec.options || {}, meta)
if (result === undefined || (result && result.code === undefined)) {
return sourceCode
} else if (result.code) {
return result.code.toString()
} else {
throw new Error('Could not find a property `.code` on the transpilation results of ' + filePath)
}
} else {
const err = new Error("Could not resolve transpiler '" + spec.transpiler + "' from '" + spec._config.path + "'")
throw err
}
}
getMetadata (spec) {
return {
name: spec._config.name,
path: spec._config.path,
meta: spec._config.meta
}
}
getTranspilerPath (spec) {
Resolve = Resolve || require('resolve')
return Resolve.sync(spec.transpiler, {
basedir: spec._config.path,
extensions: Object.keys(require.extensions)
})
}
getTranspiler (spec) {
const transpilerPath = this.getTranspilerPath(spec)
if (transpilerPath) {
const transpiler = require(transpilerPath)
this.transpilerPaths[transpilerPath] = true
return transpiler
}
}
}
module.exports = PackageTranspilationRegistry

View File

@@ -6,6 +6,7 @@ CSON = require 'season'
fs = require 'fs-plus'
{Emitter, CompositeDisposable} = require 'event-kit'
CompileCache = require './compile-cache'
ModuleCache = require './module-cache'
ScopedProperties = require './scoped-properties'
BufferedProcess = require './buffered-process'
@@ -23,6 +24,7 @@ class Package
mainModulePath: null
resolvedMainModulePath: false
mainModule: null
mainInitialized: false
mainActivated: false
###
@@ -86,6 +88,7 @@ class Package
@loadStylesheets()
@registerDeserializerMethods()
@activateCoreStartupServices()
@registerTranspilerConfig()
@configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata()
@settingsPromise = @loadSettings()
if @shouldRequireMainModuleOnLoad() and not @mainModule?
@@ -94,6 +97,9 @@ class Package
@handleError("Failed to load the #{@name} package", error)
this
unload: ->
@unregisterTranspilerConfig()
shouldRequireMainModuleOnLoad: ->
not (
@metadata.deserializers? or
@@ -109,8 +115,24 @@ class Package
@menus = []
@grammars = []
@settings = []
@mainInitialized = false
@mainActivated = false
initializeIfNeeded: ->
return if @mainInitialized
@measure 'initializeTime', =>
try
# The main module's `initialize()` method is guaranteed to be called
# before its `activate()`. This gives you a chance to handle the
# serialized package state before the package's derserializers and view
# providers are used.
@requireMainModule() unless @mainModule?
@mainModule.initialize?(@packageManager.getPackageState(@name) ? {})
@mainInitialized = true
catch error
@handleError("Failed to initialize the #{@name} package", error)
return
activate: ->
@grammarsPromise ?= @loadGrammars()
@activationPromise ?=
@@ -135,10 +157,13 @@ class Package
@registerViewProviders()
@activateStylesheets()
if @mainModule? and not @mainActivated
@initializeIfNeeded()
@mainModule.activateConfig?()
@mainModule.activate?(@packageManager.getPackageState(@name) ? {})
@mainActivated = true
@activateServices()
@activationCommandSubscriptions?.dispose()
@activationHookSubscriptions?.dispose()
catch error
@handleError("Failed to activate the #{@name} package", error)
@@ -247,6 +272,14 @@ class Package
@activationDisposables.add @packageManager.serviceHub.consume(name, version, @mainModule[methodName].bind(@mainModule))
return
registerTranspilerConfig: ->
if @metadata.atomTranspilers
CompileCache.addTranspilerConfigForPath(@path, @name, @metadata, @metadata.atomTranspilers)
unregisterTranspilerConfig: ->
if @metadata.atomTranspilers
CompileCache.removeTranspilerConfigForPath(@path)
loadKeymaps: ->
if @bundledPackage and @packageManager.packagesCache[@name]?
@keymaps = (["#{@packageManager.resourcePath}#{path.sep}#{keymapPath}", keymapObject] for keymapPath, keymapObject of @packageManager.packagesCache[@name].keymaps)
@@ -288,6 +321,7 @@ class Package
deserialize: (state, atomEnvironment) =>
@registerViewProviders()
@requireMainModule()
@initializeIfNeeded()
@mainModule[methodName](state, atomEnvironment)
return
@@ -305,6 +339,7 @@ class Package
@requireMainModule()
@metadata.viewProviders.forEach (methodName) =>
@viewRegistry.addViewProvider (model) =>
@initializeIfNeeded()
@mainModule[methodName](model)
@registeredViewProviders = true
@@ -407,6 +442,7 @@ class Package
@mainModule?.deactivate?()
@mainModule?.deactivateConfig?()
@mainActivated = false
@mainInitialized = false
catch e
console.error "Error deactivating package '#{@name}'", e.stack
@emitter.emit 'did-deactivate'

View File

@@ -234,6 +234,39 @@ class Pane extends Model
onDidChangeActiveItem: (callback) ->
@emitter.on 'did-change-active-item', callback
# Public: Invoke the given callback when {::activateNextRecentlyUsedItem}
# has been called, either initiating or continuing a forward MRU traversal of
# pane items.
#
# * `callback` {Function} to be called with when the active item changes.
# * `nextRecentlyUsedItem` The next MRU item, now being set active
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onChooseNextMRUItem: (callback) ->
@emitter.on 'choose-next-mru-item', callback
# Public: Invoke the given callback when {::activatePreviousRecentlyUsedItem}
# has been called, either initiating or continuing a reverse MRU traversal of
# pane items.
#
# * `callback` {Function} to be called with when the active item changes.
# * `previousRecentlyUsedItem` The previous MRU item, now being set active
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onChooseLastMRUItem: (callback) ->
@emitter.on 'choose-last-mru-item', callback
# Public: Invoke the given callback when {::moveActiveItemToTopOfStack}
# has been called, terminating an MRU traversal of pane items and moving the
# current active item to the top of the stack. Typically bound to a modifier
# (e.g. CTRL) key up event.
#
# * `callback` {Function} to be called with when the MRU traversal is done.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDoneChoosingMRUItem: (callback) ->
@emitter.on 'done-choosing-mru-item', callback
# Public: Invoke the given callback with the current and future values of
# {::getActiveItem}.
#
@@ -334,6 +367,7 @@ class Pane extends Model
@itemStackIndex = @itemStack.length if @itemStackIndex is 0
@itemStackIndex = @itemStackIndex - 1
nextRecentlyUsedItem = @itemStack[@itemStackIndex]
@emitter.emit 'choose-next-mru-item', nextRecentlyUsedItem
@setActiveItem(nextRecentlyUsedItem, modifyStack: false)
# Makes the previous item in the itemStack active.
@@ -343,12 +377,15 @@ class Pane extends Model
@itemStackIndex = -1
@itemStackIndex = @itemStackIndex + 1
previousRecentlyUsedItem = @itemStack[@itemStackIndex]
@emitter.emit 'choose-last-mru-item', previousRecentlyUsedItem
@setActiveItem(previousRecentlyUsedItem, modifyStack: false)
# Moves the active item to the end of the itemStack once the ctrl key is lifted
moveActiveItemToTopOfStack: ->
delete @itemStackIndex
@addItemToStack(@activeItem)
@emitter.emit 'done-choosing-mru-item'
# Public: Makes the next item active.
activateNextItem: ->
@@ -583,7 +620,7 @@ class Pane extends Model
chosen = @applicationDelegate.confirm
message: message
detailedMessage: "Your changes will be lost if you close this item without saving."
buttons: [saveButtonText, "Cancel", "Don't save"]
buttons: [saveButtonText, "Cancel", "Don't Save"]
switch chosen
when 0 then saveFn(item, saveError)
when 1 then false

View File

@@ -70,7 +70,15 @@ class Project extends Model
serialize: (options={}) ->
deserializer: 'Project'
paths: @getPaths()
buffers: _.compact(@buffers.map (buffer) -> buffer.serialize({markerLayers: options.isUnloading is true}) if buffer.isRetained())
buffers: _.compact(@buffers.map (buffer) ->
if buffer.isRetained()
state = buffer.serialize({markerLayers: options.isUnloading is true})
# Skip saving large buffer text unless unloading to avoid blocking main thread
if not options.isUnloading and state.text.length > 2 * 1024 * 1024
delete state.text
delete state.digestWhenLastPersisted
state
)
###
Section: Event Subscription
@@ -225,11 +233,11 @@ class Project extends Model
uri
else
if fs.isAbsolute(uri)
path.normalize(fs.absolute(uri))
path.normalize(fs.resolveHome(uri))
# TODO: what should we do here when there are multiple directories?
else if projectPath = @getPaths()[0]
path.normalize(fs.absolute(path.join(projectPath, uri)))
path.normalize(fs.resolveHome(path.join(projectPath, uri)))
else
undefined

View File

@@ -0,0 +1,76 @@
/** @babel */
import SelectListView from 'atom-select-list'
export default class ReopenProjectListView {
constructor (callback) {
this.callback = callback
this.selectListView = new SelectListView({
emptyMessage: 'No projects in history.',
itemsClassList: ['mark-active'],
items: [],
filterKeyForItem: (project) => project.name,
elementForItem: (project) => {
let element = document.createElement('li')
if (project.name === this.currentProjectName) {
element.classList.add('active')
}
element.textContent = project.name
return element
},
didConfirmSelection: (project) => {
this.cancel()
this.callback(project.value)
},
didCancelSelection: () => {
this.cancel()
}
})
this.selectListView.element.classList.add('reopen-project')
}
get element () {
return this.selectListView.element
}
dispose () {
this.cancel()
return this.selectListView.destroy()
}
cancel () {
if (this.panel != null) {
this.panel.destroy()
}
this.panel = null
this.currentProjectName = null
if (this.previouslyFocusedElement) {
this.previouslyFocusedElement.focus()
this.previouslyFocusedElement = null
}
}
attach () {
this.previouslyFocusedElement = document.activeElement
if (this.panel == null) {
this.panel = atom.workspace.addModalPanel({item: this})
}
this.selectListView.focus()
this.selectListView.reset()
}
async toggle () {
if (this.panel != null) {
this.cancel()
} else {
this.currentProjectName = atom.project != null ? this.makeName(atom.project.getPaths()) : null
const projects = atom.history.getProjects().map(p => ({ name: this.makeName(p.paths), value: p.paths }))
await this.selectListView.update({items: projects})
this.attach()
}
}
makeName (paths) {
return paths.join(', ')
}
}

View File

@@ -0,0 +1,124 @@
/** @babel */
import {CompositeDisposable} from 'event-kit'
import path from 'path'
export default class ReopenProjectMenuManager {
constructor ({menu, commands, history, config, open}) {
this.menuManager = menu
this.historyManager = history
this.config = config
this.open = open
this.projects = []
this.subscriptions = new CompositeDisposable()
this.subscriptions.add(
history.onDidChangeProjects(this.update.bind(this)),
config.onDidChange('core.reopenProjectMenuCount', ({oldValue, newValue}) => {
this.update()
}),
commands.add('atom-workspace', { 'application:reopen-project': this.reopenProjectCommand.bind(this) })
)
}
reopenProjectCommand (e) {
if (e.detail != null && e.detail.index != null) {
this.open(this.projects[e.detail.index].paths)
} else {
this.createReopenProjectListView()
}
}
createReopenProjectListView () {
if (this.reopenProjectListView == null) {
const ReopenProjectListView = require('./reopen-project-list-view')
this.reopenProjectListView = new ReopenProjectListView(paths => {
if (paths != null) {
this.open(paths)
}
})
}
this.reopenProjectListView.toggle()
}
update () {
this.disposeProjectMenu()
this.projects = this.historyManager.getProjects().slice(0, this.config.get('core.reopenProjectMenuCount'))
const newMenu = ReopenProjectMenuManager.createProjectsMenu(this.projects)
this.lastProjectMenu = this.menuManager.add([newMenu])
this.updateWindowsJumpList()
}
updateWindowsJumpList () {
if (process.platform !== 'win32') return
if (this.app === undefined) {
this.app = require('remote').app
}
this.app.setJumpList([
{
type: 'custom',
name: 'Recent Projects',
items: this.projects.map(project =>
({
type: 'task',
title: project.paths.map(ReopenProjectMenuManager.betterBaseName).join(', '),
description: project.paths.map(path => `${ReopenProjectMenuManager.betterBaseName(path)} (${path})`).join(' '),
program: process.execPath,
args: project.paths.map(path => `"${path}"`).join(' '),
iconPath: path.join(path.dirname(process.execPath), 'resources', 'cli', 'folder.ico'),
iconIndex: 0
})
)
},
{ type: 'recent' },
{ items: [
{type: 'task', title: 'New Window', program: process.execPath, args: '--new-window', description: 'Opens a new Atom window'}
]}
])
}
dispose () {
this.subscriptions.dispose()
this.disposeProjectMenu()
if (this.reopenProjectListView != null) {
this.reopenProjectListView.dispose()
}
}
disposeProjectMenu () {
if (this.lastProjectMenu) {
this.lastProjectMenu.dispose()
this.lastProjectMenu = null
}
}
static createProjectsMenu (projects) {
return {
label: 'File',
submenu: [
{
label: 'Reopen Project',
submenu: projects.map((project, index) => ({
label: this.createLabel(project),
command: 'application:reopen-project',
commandDetail: {index: index}
}))
}
]
}
}
static createLabel (project) {
return project.paths.length === 1
? project.paths[0]
: project.paths.map(this.betterBaseName).join(', ')
}
static betterBaseName (directory) {
// Handles Windows roots better than path.basename which returns '' for 'd:' and 'd:\'
const match = directory.match(/^([a-z]:)[\\]?$/i)
return match ? match[1] + '\\' : path.basename(directory)
}
}

View File

@@ -366,7 +366,7 @@ class Selection extends Model
insertText: (text, options={}) ->
oldBufferRange = @getBufferRange()
wasReversed = @isReversed()
@clear()
@clear(options)
autoIndentFirstLine = false
precedingText = @editor.getTextInRange([[oldBufferRange.start.row, 0], oldBufferRange.start])
@@ -403,7 +403,7 @@ class Selection extends Model
else if options.autoDecreaseIndent and NonWhitespaceRegExp.test(text)
@editor.autoDecreaseIndentForBufferRow(newBufferRange.start.row)
@autoscroll() if @isLastSelection()
@autoscroll() if options.autoscroll ? @isLastSelection()
newBufferRange

View File

@@ -270,7 +270,8 @@ function transformDeprecatedShadowDOMSelectors (css, context) {
}
} else {
if (previousNodeIsAtomTextEditor && node.type === 'pseudo' && node.value === '::shadow') {
selector.removeChild(node)
node.type = 'className'
node.value = '.editor'
targetsAtomTextEditorShadow = true
}
}

View File

@@ -42,7 +42,7 @@ class TextEditorComponent
@assert domNode?, "TextEditorComponent::domNode was set to null."
@domNodeValue = domNode
constructor: ({@editor, @hostElement, tileSize, @views, @themes, @styles, @assert}) ->
constructor: ({@editor, @hostElement, tileSize, @views, @themes, @styles, @assert, hiddenInputElement}) ->
@tileSize = tileSize if tileSize?
@disposables = new CompositeDisposable
@@ -70,8 +70,12 @@ class TextEditorComponent
@scrollViewNode.classList.add('scroll-view')
@domNode.appendChild(@scrollViewNode)
@hiddenInputComponent = new InputComponent
@scrollViewNode.appendChild(@hiddenInputComponent.getDomNode())
@hiddenInputComponent = new InputComponent(hiddenInputElement)
@scrollViewNode.appendChild(hiddenInputElement)
# Add a getModel method to the hidden input component to make it easy to
# access the editor in response to DOM events or when using
# document.activeElement.
hiddenInputElement.getModel = => @editor
@linesComponent = new LinesComponent({@presenter, @domElementPool, @assert, @grammars, @views})
@scrollViewNode.appendChild(@linesComponent.getDomNode())
@@ -342,7 +346,6 @@ class TextEditorComponent
focused: ->
if @mounted
@presenter.setFocused(true)
@hiddenInputComponent.getDomNode().focus()
blurred: ->
if @mounted
@@ -416,7 +419,6 @@ class TextEditorComponent
onScrollViewScroll: =>
if @mounted
console.warn "TextEditorScrollView scrolled when it shouldn't have."
@scrollViewNode.scrollTop = 0
@scrollViewNode.scrollLeft = 0
@@ -612,7 +614,7 @@ class TextEditorComponent
screenRange = new Range(startPosition, startPosition).union(initialRange)
@editor.getLastSelection().setScreenRange(screenRange, reversed: true, autoscroll: false, preserveFolds: true)
else
endPosition = [dragRow + 1, 0]
endPosition = @editor.clipScreenPosition([dragRow + 1, 0], clipDirection: 'backward')
screenRange = new Range(endPosition, endPosition).union(initialRange)
@editor.getLastSelection().setScreenRange(screenRange, reversed: false, autoscroll: false, preserveFolds: true)
@@ -905,7 +907,7 @@ class TextEditorComponent
screenRowForNode: (node) ->
while node?
if screenRow = node.dataset.screenRow
if screenRow = node.dataset?.screenRow
return parseInt(screenRow)
node = node.parentElement
null

View File

@@ -25,8 +25,17 @@ class TextEditorElement extends HTMLElement
@emitter = new Emitter
@subscriptions = new CompositeDisposable
@hiddenInputElement = document.createElement('input')
@hiddenInputElement.classList.add('hidden-input')
@hiddenInputElement.setAttribute('tabindex', -1)
@hiddenInputElement.setAttribute('data-react-skip-selection-restoration', true)
@hiddenInputElement.style['-webkit-transform'] = 'translateZ(0)'
@hiddenInputElement.addEventListener 'paste', (event) -> event.preventDefault()
@addEventListener 'focus', @focused.bind(this)
@addEventListener 'blur', @blurred.bind(this)
@hiddenInputElement.addEventListener 'focus', @focused.bind(this)
@hiddenInputElement.addEventListener 'blur', @inputNodeBlurred.bind(this)
@classList.add('editor')
@setAttribute('tabindex', -1)
@@ -117,12 +126,10 @@ class TextEditorElement extends HTMLElement
themes: @themes
styles: @styles
workspace: @workspace
assert: @assert
assert: @assert,
hiddenInputElement: @hiddenInputElement
)
@rootElement.appendChild(@component.getDomNode())
inputNode = @component.hiddenInputComponent.getDomNode()
inputNode.addEventListener 'focus', @focused.bind(this)
inputNode.addEventListener 'blur', @inputNodeBlurred.bind(this)
unmountComponent: ->
if @component?
@@ -132,16 +139,17 @@ class TextEditorElement extends HTMLElement
focused: (event) ->
@component?.focused()
@hiddenInputElement.focus()
blurred: (event) ->
if event.relatedTarget is @component?.hiddenInputComponent.getDomNode()
if event.relatedTarget is @hiddenInputElement
event.stopImmediatePropagation()
return
@component?.blurred()
inputNodeBlurred: (event) ->
if event.relatedTarget isnt this
@dispatchEvent(new FocusEvent('blur', bubbles: false))
@dispatchEvent(new FocusEvent('blur', relatedTarget: event.relatedTarget, bubbles: false))
addGrammarScopeAttribute: ->
@dataset.grammar = @model.getGrammar()?.scopeName?.replace(/\./g, ' ')

View File

@@ -306,9 +306,6 @@ class TextEditorPresenter
getEndTileRow: ->
@tileForRow(@endRow ? 0)
isValidScreenRow: (screenRow) ->
screenRow >= 0 and screenRow < @model.getApproximateScreenLineCount()
getScreenRowsToRender: ->
startRow = @getStartTileRow()
endRow = @getEndTileRow() + @tileSize
@@ -320,7 +317,7 @@ class TextEditorPresenter
if @screenRowsToMeasure?
screenRows.push(@screenRowsToMeasure...)
screenRows = screenRows.filter @isValidScreenRow.bind(this)
screenRows = screenRows.filter (row) -> row >= 0
screenRows.sort (a, b) -> a - b
_.uniq(screenRows, true)
@@ -395,19 +392,17 @@ class TextEditorPresenter
visibleTiles[tileStartRow] = true
zIndex++
if @mouseWheelScreenRow? and 0 <= @mouseWheelScreenRow < @model.getApproximateScreenLineCount()
mouseWheelTile = @tileForRow(@mouseWheelScreenRow)
unless visibleTiles[mouseWheelTile]?
@lineNumberGutter.tiles[mouseWheelTile].display = "none"
@state.content.tiles[mouseWheelTile].display = "none"
visibleTiles[mouseWheelTile] = true
mouseWheelTileId = @tileForRow(@mouseWheelScreenRow) if @mouseWheelScreenRow?
for id, tile of @state.content.tiles
continue if visibleTiles.hasOwnProperty(id)
delete @state.content.tiles[id]
delete @lineNumberGutter.tiles[id]
if Number(id) is mouseWheelTileId
@state.content.tiles[id].display = "none"
@lineNumberGutter.tiles[id].display = "none"
else
delete @state.content.tiles[id]
delete @lineNumberGutter.tiles[id]
updateLinesState: (tileState, screenRows) ->
tileState.lines ?= {}
@@ -456,7 +451,7 @@ class TextEditorPresenter
for decoration in @model.getOverlayDecorations()
continue unless decoration.getMarker().isValid()
{item, position, class: klass} = decoration.getProperties()
{item, position, class: klass, avoidOverflow} = decoration.getProperties()
if position is 'tail'
screenPosition = decoration.getMarker().getTailScreenPosition()
else
@@ -471,15 +466,16 @@ class TextEditorPresenter
if overlayDimensions = @overlayDimensions[decoration.id]
{itemWidth, itemHeight, contentMargin} = overlayDimensions
rightDiff = left + itemWidth + contentMargin - @windowWidth
left -= rightDiff if rightDiff > 0
if avoidOverflow isnt false
rightDiff = left + itemWidth + contentMargin - @windowWidth
left -= rightDiff if rightDiff > 0
leftDiff = left + contentMargin
left -= leftDiff if leftDiff < 0
leftDiff = left + contentMargin
left -= leftDiff if leftDiff < 0
if top + itemHeight > @windowHeight and
top - (itemHeight + @lineHeight) >= 0
top -= itemHeight + @lineHeight
if top + itemHeight > @windowHeight and
top - (itemHeight + @lineHeight) >= 0
top -= itemHeight + @lineHeight
pixelPosition.top = top
pixelPosition.left = left
@@ -498,7 +494,10 @@ class TextEditorPresenter
return
updateLineNumberGutterState: ->
@lineNumberGutter.maxLineNumberDigits = @model.getLineCount().toString().length
@lineNumberGutter.maxLineNumberDigits = Math.max(
2,
@model.getLineCount().toString().length
)
updateCommonGutterState: ->
@sharedGutterStyles.backgroundColor = if @gutterBackgroundColor isnt "rgba(0, 0, 0, 0)"
@@ -601,7 +600,8 @@ class TextEditorPresenter
line = @linesByScreenRow.get(screenRow)
continue unless line?
lineId = line.id
{bufferRow, softWrappedAtStart: softWrapped} = @displayLayer.softWrapDescriptorForScreenRow(screenRow)
{row: bufferRow, column: bufferColumn} = @displayLayer.translateScreenPosition(Point(screenRow, 0))
softWrapped = bufferColumn isnt 0
foldable = not softWrapped and @model.isFoldableAtBufferRow(bufferRow)
decorationClasses = @lineNumberDecorationClassesForRow(screenRow)
blockDecorationsBeforeCurrentScreenRowHeight = @lineTopIndex.pixelPositionAfterBlocksForRow(screenRow) - @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRow)
@@ -1006,8 +1006,7 @@ class TextEditorPresenter
@lineHeight? and @baseCharacterWidth?
pixelPositionForScreenPosition: (screenPosition) ->
position =
@linesYardstick.pixelPositionForScreenPosition(screenPosition)
position = @linesYardstick.pixelPositionForScreenPosition(screenPosition)
position.top -= @getScrollTop()
position.left -= @getScrollLeft()
@@ -1145,7 +1144,9 @@ class TextEditorPresenter
@lineNumberDecorationsByScreenRow[screenRow] ?= {}
@lineNumberDecorationsByScreenRow[screenRow][decorationId] = properties
else
for row in [screenRange.start.row..screenRange.end.row] by 1
startRow = Math.max(screenRange.start.row, @getStartTileRow())
endRow = Math.min(screenRange.end.row, @getEndTileRow() + @tileSize)
for row in [startRow..endRow] by 1
continue if properties.onlyHead and row isnt headScreenPosition.row
continue if omitLastRow and row is screenRange.end.row
@@ -1230,13 +1231,14 @@ class TextEditorPresenter
screenRange.end.column = 0
repositionRegionWithinTile: (region, tileStartRow) ->
region.top += @scrollTop - @lineTopIndex.pixelPositionBeforeBlocksForRow(tileStartRow)
region.left += @scrollLeft
region.top += @scrollTop - @lineTopIndex.pixelPositionBeforeBlocksForRow(tileStartRow)
buildHighlightRegions: (screenRange) ->
lineHeightInPixels = @lineHeight
startPixelPosition = @pixelPositionForScreenPosition(screenRange.start)
endPixelPosition = @pixelPositionForScreenPosition(screenRange.end)
startPixelPosition.left += @scrollLeft
endPixelPosition.left += @scrollLeft
spannedRows = screenRange.end.row - screenRange.start.row + 1
regions = []
@@ -1413,11 +1415,10 @@ class TextEditorPresenter
@emitDidUpdateState()
pauseCursorBlinking: ->
if @isCursorBlinking()
@stopBlinkingCursors(true)
@startBlinkingCursorsAfterDelay ?= _.debounce(@startBlinkingCursors, @getCursorBlinkResumeDelay())
@startBlinkingCursorsAfterDelay()
@emitDidUpdateState()
@stopBlinkingCursors(true)
@startBlinkingCursorsAfterDelay ?= _.debounce(@startBlinkingCursors, @getCursorBlinkResumeDelay())
@startBlinkingCursorsAfterDelay()
@emitDidUpdateState()
requestAutoscroll: (position) ->
@pendingScrollLogicalPosition = position

View File

@@ -114,9 +114,6 @@ class TextEditor extends Model
throw error
state.buffer = state.tokenizedBuffer.buffer
if state.displayLayer = state.buffer.getDisplayLayer(state.displayLayerId)
state.selectionsMarkerLayer = state.displayLayer.getMarkerLayer(state.selectionsMarkerLayerId)
state.assert = atomEnvironment.assert.bind(atomEnvironment)
editor = new this(state)
if state.registered
@@ -167,22 +164,24 @@ class TextEditor extends Model
grammar, tabLength, @buffer, @largeFileMode, @assert
})
displayLayerParams = {
invisibles: @getInvisibles(),
softWrapColumn: @getSoftWrapColumn(),
showIndentGuides: not @isMini() and @doesShowIndentGuide(),
atomicSoftTabs: params.atomicSoftTabs ? true,
tabLength: tabLength,
ratioForCharacter: @ratioForCharacter.bind(this),
isWrapBoundary: isWrapBoundary,
foldCharacter: ZERO_WIDTH_NBSP,
softWrapHangingIndent: params.softWrapHangingIndentLength ? 0
}
unless @displayLayer?
displayLayerParams = {
invisibles: @getInvisibles(),
softWrapColumn: @getSoftWrapColumn(),
showIndentGuides: not @isMini() and @doesShowIndentGuide(),
atomicSoftTabs: params.atomicSoftTabs ? true,
tabLength: tabLength,
ratioForCharacter: @ratioForCharacter.bind(this),
isWrapBoundary: isWrapBoundary,
foldCharacter: ZERO_WIDTH_NBSP,
softWrapHangingIndent: params.softWrapHangingIndentLength ? 0
}
if @displayLayer?
@displayLayer.reset(displayLayerParams)
else
@displayLayer = @buffer.addDisplayLayer(displayLayerParams)
if @displayLayer = @buffer.getDisplayLayer(params.displayLayerId)
@displayLayer.reset(displayLayerParams)
@selectionsMarkerLayer = @displayLayer.getMarkerLayer(params.selectionsMarkerLayerId)
else
@displayLayer = @buffer.addDisplayLayer(displayLayerParams)
@backgroundWorkHandle = requestIdleCallback(@doBackgroundWork)
@disposables.add new Disposable =>
@@ -272,12 +271,12 @@ class TextEditor extends Model
when 'softWrapAtPreferredLineLength'
if value isnt @softWrapAtPreferredLineLength
@softWrapAtPreferredLineLength = value
displayLayerParams.softWrapColumn = @getSoftWrapColumn() if @isSoftWrapped()
displayLayerParams.softWrapColumn = @getSoftWrapColumn()
when 'preferredLineLength'
if value isnt @preferredLineLength
@preferredLineLength = value
displayLayerParams.softWrapColumn = @getSoftWrapColumn() if @isSoftWrapped()
displayLayerParams.softWrapColumn = @getSoftWrapColumn()
when 'mini'
if value isnt @mini
@@ -322,12 +321,12 @@ class TextEditor extends Model
when 'editorWidthInChars'
if value > 0 and value isnt @editorWidthInChars
@editorWidthInChars = value
displayLayerParams.softWrapColumn = @getSoftWrapColumn() if @isSoftWrapped()
displayLayerParams.softWrapColumn = @getSoftWrapColumn()
when 'width'
if value isnt @width
@width = value
displayLayerParams.softWrapColumn = @getSoftWrapColumn() if @isSoftWrapped()
displayLayerParams.softWrapColumn = @getSoftWrapColumn()
when 'scrollPastEnd'
if value isnt @scrollPastEnd
@@ -346,8 +345,7 @@ class TextEditor extends Model
else
throw new TypeError("Invalid TextEditor parameter: '#{param}'")
if Object.keys(displayLayerParams).length > 0
@displayLayer.reset(displayLayerParams)
@displayLayer.reset(displayLayerParams)
if @editorElement?
@editorElement.views.getNextUpdatePromise()
@@ -412,14 +410,15 @@ class TextEditor extends Model
destroyed: ->
@disposables.dispose()
@displayLayer.destroy()
@disposables.dispose()
@tokenizedBuffer.destroy()
selection.destroy() for selection in @selections.slice()
@selectionsMarkerLayer.destroy()
@buffer.release()
@languageMode.destroy()
@gutterContainer.destroy()
@emitter.emit 'did-destroy'
@emitter.clear()
@editorElement = null
@presenter = null
###
Section: Event Subscription
@@ -892,8 +891,8 @@ class TextEditor extends Model
# Determine whether the user should be prompted to save before closing
# this editor.
shouldPromptToSave: ({windowCloseRequested}={}) ->
if windowCloseRequested
shouldPromptToSave: ({windowCloseRequested, projectHasPaths}={}) ->
if windowCloseRequested and projectHasPaths
false
else
@isModified() and not @buffer.hasMultipleEditors()
@@ -982,10 +981,7 @@ class TextEditor extends Model
@bufferRowForScreenRow(screenRow)
screenRowForBufferRow: (row) ->
if @largeFileMode
row
else
@displayLayer.translateBufferPosition(Point(row, 0)).row
@displayLayer.translateBufferPosition(Point(row, 0)).row
getRightmostScreenPosition: -> @displayLayer.getRightmostScreenPosition()
@@ -1076,8 +1072,8 @@ class TextEditor extends Model
)
# Essential: For each selection, replace the selected text with a newline.
insertNewline: ->
@insertText('\n')
insertNewline: (options) ->
@insertText('\n', options)
# Essential: For each selection, if the selection is empty, delete the character
# following the cursor. Otherwise delete the selected text.
@@ -1134,13 +1130,13 @@ class TextEditor extends Model
# Don't move the last line of a multi-line selection if the selection ends at column 0
endRow--
{bufferRow: startRow} = @displayLayer.lineStartBoundaryForBufferRow(startRow)
{bufferRow: endRow} = @displayLayer.lineEndBoundaryForBufferRow(endRow)
startRow = @displayLayer.findBoundaryPrecedingBufferRow(startRow)
endRow = @displayLayer.findBoundaryFollowingBufferRow(endRow + 1)
linesRange = new Range(Point(startRow, 0), Point(endRow, 0))
# If selected line range is preceded by a fold, one line above on screen
# could be multiple lines in the buffer.
{bufferRow: precedingRow} = @displayLayer.lineStartBoundaryForBufferRow(startRow - 1)
precedingRow = @displayLayer.findBoundaryPrecedingBufferRow(startRow - 1)
insertDelta = linesRange.start.row - precedingRow
# Any folds in the text that is moved will need to be re-created.
@@ -1196,15 +1192,15 @@ class TextEditor extends Model
# Don't move the last line of a multi-line selection if the selection ends at column 0
endRow--
{bufferRow: startRow} = @displayLayer.lineStartBoundaryForBufferRow(startRow)
{bufferRow: endRow} = @displayLayer.lineEndBoundaryForBufferRow(endRow)
startRow = @displayLayer.findBoundaryPrecedingBufferRow(startRow)
endRow = @displayLayer.findBoundaryFollowingBufferRow(endRow + 1)
linesRange = new Range(Point(startRow, 0), Point(endRow, 0))
# If selected line range is followed by a fold, one line below on screen
# could be multiple lines in the buffer. But at the same time, if the
# next buffer row is wrapped, one line in the buffer can represent many
# screen rows.
{bufferRow: followingRow} = @displayLayer.lineEndBoundaryForBufferRow(endRow)
followingRow = Math.min(@buffer.getLineCount(), @displayLayer.findBoundaryFollowingBufferRow(endRow + 1))
insertDelta = followingRow - linesRange.end.row
# Any folds in the text that is moved will need to be re-created.
@@ -1276,30 +1272,44 @@ class TextEditor extends Model
@setSelectedBufferRanges(translatedRanges)
# Duplicate the most recent cursor's current line.
duplicateLines: ->
@transact =>
for selection in @getSelectionsOrderedByBufferPosition().reverse()
selectedBufferRange = selection.getBufferRange()
if selection.isEmpty()
{start} = selection.getScreenRange()
selection.setScreenRange([[start.row, 0], [start.row + 1, 0]], preserveFolds: true)
selections = @getSelectionsOrderedByBufferPosition()
previousSelectionRanges = []
[startRow, endRow] = selection.getBufferRowRange()
i = selections.length - 1
while i >= 0
j = i
previousSelectionRanges[i] = selections[i].getBufferRange()
if selections[i].isEmpty()
{start} = selections[i].getScreenRange()
selections[i].setScreenRange([[start.row, 0], [start.row + 1, 0]], preserveFolds: true)
[startRow, endRow] = selections[i].getBufferRowRange()
endRow++
while i > 0
[previousSelectionStartRow, previousSelectionEndRow] = selections[i - 1].getBufferRowRange()
if previousSelectionEndRow is startRow
startRow = previousSelectionStartRow
previousSelectionRanges[i - 1] = selections[i - 1].getBufferRange()
i--
else
break
intersectingFolds = @displayLayer.foldsIntersectingBufferRange([[startRow, 0], [endRow, 0]])
rangeToDuplicate = [[startRow, 0], [endRow, 0]]
textToDuplicate = @getTextInBufferRange(rangeToDuplicate)
textToDuplicate = @getTextInBufferRange([[startRow, 0], [endRow, 0]])
textToDuplicate = '\n' + textToDuplicate if endRow > @getLastBufferRow()
@buffer.insert([endRow, 0], textToDuplicate)
delta = endRow - startRow
selection.setBufferRange(selectedBufferRange.translate([delta, 0]))
insertedRowCount = endRow - startRow
for k in [i..j] by 1
selections[k].setBufferRange(previousSelectionRanges[k].translate([insertedRowCount, 0]))
for fold in intersectingFolds
foldRange = @displayLayer.bufferRangeForFold(fold)
@displayLayer.foldBufferRange(foldRange.translate([delta, 0]))
return
@displayLayer.foldBufferRange(foldRange.translate([insertedRowCount, 0]))
i--
replaceSelectedText: (options={}, fn) ->
{selectWordIfEmpty} = options
@@ -1740,10 +1750,14 @@ class TextEditor extends Model
# * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied
# if the associated `DisplayMarker` is non-empty. Only applicable to the
# `gutter`, `line`, and `line-number` types.
# * `position` (optional) Only applicable to decorations of type `overlay` and `block`,
# controls where the view is positioned relative to the `TextEditorMarker`.
# * `position` (optional) Only applicable to decorations of type `overlay` and `block`.
# 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.
# * `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
decorateMarker: (marker, decorationParams) ->
@@ -2916,11 +2930,7 @@ class TextEditor extends Model
# Essential: Determine whether lines in this editor are soft-wrapped.
#
# Returns a {Boolean}.
isSoftWrapped: ->
if @largeFileMode
false
else
@softWrapped
isSoftWrapped: -> @softWrapped
# Essential: Enable or disable soft wrapping for this editor.
#

View File

@@ -178,7 +178,8 @@ class ThemeManager
@requireStylesheet(nativeStylesheetPath)
stylesheetElementForId: (id) ->
document.head.querySelector("atom-styles style[source-path=\"#{id}\"]")
escapedId = id.replace(/\\/g, '\\\\')
document.head.querySelector("atom-styles style[source-path=\"#{escapedId}\"]")
resolveStylesheet: (stylesheetPath) ->
if path.extname(stylesheetPath).length > 0
@@ -231,9 +232,6 @@ class ThemeManager
applyStylesheet: (path, text) ->
@styleSheetDisposablesBySourcePath[path] = @styleManager.addStyleSheet(text, sourcePath: path)
stringToId: (string) ->
string.replace(/\\/g, '/')
activateThemes: ->
new Promise (resolve) =>
# @config.observe runs the callback once, then on subsequent changes.

View File

@@ -41,6 +41,7 @@ class TokenizedBuffer extends Model
destroyed: ->
@disposables.dispose()
@tokenizedLines.length = 0
buildIterator: ->
new TokenizedBufferIterator(this)
@@ -94,6 +95,7 @@ class TokenizedBuffer extends Model
false
retokenizeLines: ->
return unless @alive
@fullyTokenized = false
@tokenizedLines = new Array(@buffer.getLineCount())
@invalidRows = []
@@ -198,10 +200,7 @@ class TokenizedBuffer extends Model
@invalidateRow(end + delta + 1)
isFoldableAtRow: (row) ->
if @largeFileMode
false
else
@isFoldableCodeAtRow(row) or @isFoldableCommentAtRow(row)
@isFoldableCodeAtRow(row) or @isFoldableCommentAtRow(row)
# Returns a {Boolean} indicating whether the given buffer row starts
# a a foldable row range due to the code's indentation patterns.

View File

@@ -56,6 +56,7 @@ class TooltipManager
{delay: {show: 1000, hide: 100}}
constructor: ({@keymapManager, @viewRegistry}) ->
@tooltips = new Map()
# Essential: Add a tooltip to the given element.
#
@@ -129,19 +130,42 @@ class TooltipManager
tooltip = new Tooltip(target, options, @viewRegistry)
if not @tooltips.has(target)
@tooltips.set(target, [])
@tooltips.get(target).push(tooltip)
hideTooltip = ->
tooltip.leave(currentTarget: target)
tooltip.hide()
window.addEventListener('resize', hideTooltip)
disposable = new Disposable ->
disposable = new Disposable =>
window.removeEventListener('resize', hideTooltip)
hideTooltip()
tooltip.destroy()
if @tooltips.has(target)
tooltipsForTarget = @tooltips.get(target)
index = tooltipsForTarget.indexOf(tooltip)
if index isnt -1
tooltipsForTarget.splice(index, 1)
if tooltipsForTarget.length is 0
@tooltips.delete(target)
disposable
# Extended: Find the tooltips that have been applied to the given element.
#
# * `target` The `HTMLElement` to find tooltips on.
#
# Returns an {Array} of `Tooltip` objects that match the `target`.
findTooltips: (target) ->
if @tooltips.has(target)
@tooltips.get(target).slice()
else
[]
humanizeKeystrokes = (keystroke) ->
keystrokes = keystroke.split(' ')
keystrokes = (_.humanizeKeystroke(stroke) for stroke in keystrokes)

View File

@@ -7,6 +7,8 @@ const listen = require('./delegated-listener')
// This tooltip class is derived from Bootstrap 3, but modified to not require
// jQuery, which is an expensive dependency we want to eliminate.
var followThroughTimer = null
var Tooltip = function (element, options, viewRegistry) {
this.options = null
this.enabled = null
@@ -21,7 +23,7 @@ var Tooltip = function (element, options, viewRegistry) {
Tooltip.VERSION = '3.3.5'
Tooltip.TRANSITION_DURATION = 150
Tooltip.FOLLOW_THROUGH_DURATION = 300
Tooltip.DEFAULTS = {
animation: true,
@@ -151,7 +153,11 @@ Tooltip.prototype.enter = function (event) {
this.hoverState = 'in'
if (!this.options.delay || !this.options.delay.show) return this.show()
if (!this.options.delay ||
!this.options.delay.show ||
followThroughTimer) {
return this.show()
}
this.timeout = setTimeout(function () {
if (this.hoverState === 'in') this.show()
@@ -343,6 +349,14 @@ Tooltip.prototype.hide = function (callback) {
this.hoverState = null
clearTimeout(followThroughTimer)
followThroughTimer = setTimeout(
function () {
followThroughTimer = null
},
Tooltip.FOLLOW_THROUGH_DURATION
)
return this
}

View File

@@ -1,7 +1,7 @@
/** @babel */
import fs from 'fs'
import {spawnSync} from 'child_process'
import childProcess from 'child_process'
const ENVIRONMENT_VARIABLES_TO_PRESERVE = new Set([
'NODE_ENV',
@@ -15,12 +15,14 @@ const PLATFORMS_KNOWN_TO_WORK = new Set([
'linux'
])
function updateProcessEnv (launchEnv) {
async function updateProcessEnv (launchEnv) {
let envToAssign
if (launchEnv && shouldGetEnvFromShell(launchEnv)) {
envToAssign = getEnvFromShell(launchEnv)
} else if (launchEnv && launchEnv.PWD) {
envToAssign = launchEnv
if (launchEnv) {
if (shouldGetEnvFromShell(launchEnv)) {
envToAssign = await getEnvFromShell(launchEnv)
} else if (launchEnv.PWD) {
envToAssign = launchEnv
}
}
if (envToAssign) {
@@ -58,24 +60,64 @@ function shouldGetEnvFromShell (env) {
return true
}
function getEnvFromShell (env) {
if (!shouldGetEnvFromShell(env)) {
return
}
let {stdout} = spawnSync(env.SHELL, ['-ilc', 'command env'], {encoding: 'utf8'})
if (stdout) {
let result = {}
for (let line of stdout.split('\n')) {
if (line.includes('=')) {
let components = line.split('=')
let key = components.shift()
let value = components.join('=')
result[key] = value
async function getEnvFromShell (env) {
let {stdout, error} = await new Promise((resolve) => {
let child
let error
let stdout = ''
let done = false
const cleanup = () => {
if (!done && child) {
child.kill()
done = true
}
}
return result
process.once('exit', cleanup)
setTimeout(() => {
cleanup()
}, 5000)
child = childProcess.spawn(env.SHELL, ['-ilc', 'command env'], {encoding: 'utf8', detached: true, stdio: ['ignore', 'pipe', process.stderr]})
const buffers = []
child.on('error', (e) => {
done = true
error = e
})
child.stdout.on('data', (data) => {
buffers.push(data)
})
child.on('close', (code, signal) => {
done = true
process.removeListener('exit', cleanup)
if (buffers.length) {
stdout = Buffer.concat(buffers).toString('utf8')
}
resolve({stdout, error})
})
})
if (error) {
if (error.handle) {
error.handle()
}
console.log('warning: ' + env.SHELL + ' -ilc "command env" failed with signal (' + error.signal + ')')
console.log(error)
}
if (!stdout || stdout.trim() === '') {
return null
}
let result = {}
for (let line of stdout.split('\n')) {
if (line.includes('=')) {
let components = line.split('=')
let key = components.shift()
let value = components.join('=')
result[key] = value
}
}
return result
}
export default { updateProcessEnv, shouldGetEnvFromShell }

View File

@@ -148,7 +148,8 @@ class WindowEventHandler
@document.body.classList.remove("fullscreen")
handleWindowBeforeunload: (event) =>
confirmed = @atomEnvironment.workspace?.confirmClose(windowCloseRequested: true)
projectHasPaths = @atomEnvironment.project.getPaths().length > 0
confirmed = @atomEnvironment.workspace?.confirmClose(windowCloseRequested: true, projectHasPaths: projectHasPaths)
if confirmed and not @reloadRequested and not @atomEnvironment.inSpecMode() and @atomEnvironment.getCurrentWindow().isWebViewFocused()
@atomEnvironment.hide()
@reloadRequested = false

View File

@@ -182,7 +182,7 @@ class Workspace extends Model
projectPath = _.find projectPaths, (projectPath) ->
itemPath is projectPath or itemPath?.startsWith(projectPath + path.sep)
itemTitle ?= "untitled"
projectPath ?= projectPaths[0]
projectPath ?= if itemPath then path.dirname(itemPath) else null
if projectPath?
projectPath = fs.tildify(projectPath)
@@ -441,7 +441,7 @@ class Workspace extends Model
# Avoid adding URLs as recent documents to work-around this Spotlight crash:
# https://github.com/atom/atom/issues/10071
if uri? and not url.parse(uri).protocol?
if uri? and (not url.parse(uri).protocol? or process.platform is 'win32')
@applicationDelegate.addRecentDocument(uri)
pane = @paneContainer.paneForURI(uri) if searchAllPanes