mirror of
https://github.com/atom/atom.git
synced 2026-02-12 07:35:14 -05:00
Merge branch 'master' into sm-hidden-all
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
62
src/atom-paths.js
Normal 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
|
||||
}
|
||||
@@ -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})
|
||||
56
src/buffered-node-process.js
Normal file
56
src/buffered-node-process.js
Normal 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
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
298
src/buffered-process.js
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
150
src/history-manager.js
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ?= {}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'))
|
||||
)
|
||||
77
src/main-process/win-shell.js
Normal file
77
src/main-process/win-shell.js
Normal 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'))
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
172
src/package-transpilation-registry.js
Normal file
172
src/package-transpilation-registry.js
Normal 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
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
76
src/reopen-project-list-view.js
Normal file
76
src/reopen-project-list-view.js
Normal 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(', ')
|
||||
}
|
||||
}
|
||||
124
src/reopen-project-menu-manager.js
Normal file
124
src/reopen-project-menu-manager.js
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, ' ')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
#
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user