mirror of
https://github.com/atom/atom.git
synced 2026-02-06 20:55:33 -05:00
Merge branch 'master' into as-ns-startup-snapshot
This commit is contained in:
@@ -2,10 +2,12 @@ _ = require 'underscore-plus'
|
||||
{screen, ipcRenderer, remote, shell, webFrame} = require 'electron'
|
||||
ipcHelpers = require './ipc-helpers'
|
||||
{Disposable} = require 'event-kit'
|
||||
{getWindowLoadSettings, setWindowLoadSettings} = require './window-load-settings-helpers'
|
||||
getWindowLoadSettings = require './get-window-load-settings'
|
||||
|
||||
module.exports =
|
||||
class ApplicationDelegate
|
||||
getWindowLoadSettings: -> getWindowLoadSettings()
|
||||
|
||||
open: (params) ->
|
||||
ipcRenderer.send('open', params)
|
||||
|
||||
@@ -109,9 +111,7 @@ class ApplicationDelegate
|
||||
ipcRenderer.send("add-recent-document", filename)
|
||||
|
||||
setRepresentedDirectoryPaths: (paths) ->
|
||||
loadSettings = getWindowLoadSettings()
|
||||
loadSettings['initialPaths'] = paths
|
||||
setWindowLoadSettings(loadSettings)
|
||||
ipcHelpers.call('window-method', 'setRepresentedDirectoryPaths', paths)
|
||||
|
||||
setAutoHideWindowMenuBar: (autoHide) ->
|
||||
ipcHelpers.call('window-method', 'setAutoHideMenuBar', autoHide)
|
||||
@@ -148,13 +148,9 @@ class ApplicationDelegate
|
||||
showMessageDialog: (params) ->
|
||||
|
||||
showSaveDialog: (params) ->
|
||||
if _.isString(params)
|
||||
params = defaultPath: params
|
||||
else
|
||||
params = _.clone(params)
|
||||
params.title ?= 'Save File'
|
||||
params.defaultPath ?= getWindowLoadSettings().initialPaths[0]
|
||||
remote.dialog.showSaveDialog remote.getCurrentWindow(), params
|
||||
if typeof params is 'string'
|
||||
params = {defaultPath: params}
|
||||
@getCurrentWindow().showSaveDialog(params)
|
||||
|
||||
playBeepSound: ->
|
||||
shell.beep()
|
||||
|
||||
@@ -11,7 +11,6 @@ Model = require './model'
|
||||
WindowEventHandler = require './window-event-handler'
|
||||
StateStore = require './state-store'
|
||||
StorageFolder = require './storage-folder'
|
||||
{getWindowLoadSettings} = require './window-load-settings-helpers'
|
||||
registerDefaultCommands = require './register-default-commands'
|
||||
{updateProcessEnv} = require './update-process-env'
|
||||
|
||||
@@ -240,16 +239,6 @@ class AtomEnvironment extends Model
|
||||
|
||||
new ReopenProjectMenuManager({@menu, @commands, @history, @config, open: (paths) => @open(pathsToOpen: paths)})
|
||||
|
||||
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)
|
||||
|
||||
checkPortableHomeWritable()
|
||||
|
||||
attachSaveStateListeners: ->
|
||||
saveState = _.debounce((=>
|
||||
window.requestIdleCallback => @saveState({isUnloading: false}) unless @unloaded
|
||||
@@ -294,13 +283,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)
|
||||
@@ -468,7 +457,7 @@ class AtomEnvironment extends Model
|
||||
#
|
||||
# Returns an {Object} containing all the load setting key/value pairs.
|
||||
getLoadSettings: ->
|
||||
getWindowLoadSettings()
|
||||
@applicationDelegate.getWindowLoadSettings()
|
||||
|
||||
###
|
||||
Section: Managing The Atom Window
|
||||
@@ -831,12 +820,17 @@ class AtomEnvironment extends Model
|
||||
Section: Private
|
||||
###
|
||||
|
||||
assert: (condition, message, callback) ->
|
||||
assert: (condition, message, callbackOrMetadata) ->
|
||||
return true if condition
|
||||
|
||||
error = new Error("Assertion failed: #{message}")
|
||||
Error.captureStackTrace(error, @assert)
|
||||
callback?(error)
|
||||
|
||||
if callbackOrMetadata?
|
||||
if typeof callbackOrMetadata is 'function'
|
||||
callbackOrMetadata?(error)
|
||||
else
|
||||
error.metadata = callbackOrMetadata
|
||||
|
||||
@emitter.emit 'did-fail-assertion', error
|
||||
|
||||
|
||||
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 process.execPath.substring(0, process.execPath.indexOf('.app') + 4)
|
||||
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
|
||||
}
|
||||
20
src/babel.js
20
src/babel.js
@@ -6,6 +6,7 @@ var defaultOptions = require('../static/babelrc.json')
|
||||
|
||||
var babel = null
|
||||
var babelVersionDirectory = null
|
||||
var options = null
|
||||
|
||||
var PREFIXES = [
|
||||
'/** @babel */',
|
||||
@@ -47,16 +48,27 @@ exports.compile = function (sourceCode, filePath) {
|
||||
var noop = function () {}
|
||||
Logger.prototype.debug = noop
|
||||
Logger.prototype.verbose = noop
|
||||
|
||||
options = {ast: false, babelrc: false}
|
||||
for (var key in defaultOptions) {
|
||||
if (key === 'plugins') {
|
||||
const plugins = []
|
||||
for (const [pluginName, pluginOptions] of defaultOptions[key]) {
|
||||
plugins.push([require.resolve(`babel-plugin-${pluginName}`), pluginOptions])
|
||||
}
|
||||
options[key] = plugins
|
||||
} else {
|
||||
options[key] = defaultOptions[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
filePath = 'file:///' + path.resolve(filePath).replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
var options = {filename: filePath}
|
||||
for (var key in defaultOptions) {
|
||||
options[key] = defaultOptions[key]
|
||||
}
|
||||
options.filename = filePath
|
||||
|
||||
return babel.transform(sourceCode, options).code
|
||||
}
|
||||
|
||||
|
||||
@@ -46,43 +46,61 @@ export default class BufferedProcess {
|
||||
// * `exit` {Function} (optional) The callback which receives a single
|
||||
// argument containing the exit status.
|
||||
// * `code` {Number}
|
||||
constructor ({command, args, options = {}, stdout, stderr, exit} = {}) {
|
||||
// * `autoStart` {Boolean} (optional) Whether the command will automatically start
|
||||
// when this BufferedProcess is created. Defaults to true. When set to false you
|
||||
// must call the `start` method to start the process.
|
||||
constructor ({command, args, options = {}, stdout, stderr, exit, autoStart = true} = {}) {
|
||||
this.emitter = new Emitter()
|
||||
this.command = command
|
||||
this.args = args
|
||||
this.options = options
|
||||
this.stdout = stdout
|
||||
this.stderr = stderr
|
||||
this.exit = exit
|
||||
if (autoStart === true) {
|
||||
this.start()
|
||||
}
|
||||
this.killed = false
|
||||
}
|
||||
|
||||
start () {
|
||||
if (this.started === true) return
|
||||
|
||||
this.started = true
|
||||
// Related to joyent/node#2318
|
||||
if (process.platform === 'win32' && !options.shell) {
|
||||
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 {
|
||||
return `\"${arg.toString().replace(/"/g, '\\"')}\"`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (/\s/.test(command)) {
|
||||
cmdArgs.unshift(`\"${command}\"`)
|
||||
} else {
|
||||
cmdArgs.unshift(command)
|
||||
}
|
||||
|
||||
cmdArgs = ['/s', '/d', '/c', `\"${cmdArgs.join(' ')}\"`]
|
||||
const cmdOptions = _.clone(options)
|
||||
cmdOptions.windowsVerbatimArguments = true
|
||||
this.spawn(this.getCmdPath(), cmdArgs, cmdOptions)
|
||||
if (process.platform === 'win32' && this.options.shell === undefined) {
|
||||
this.spawnWithEscapedWindowsArgs(this.command, this.args, this.options)
|
||||
} else {
|
||||
this.spawn(command, args, options)
|
||||
this.spawn(this.command, this.args, this.options)
|
||||
}
|
||||
this.handleEvents(this.stdout, this.stderr, this.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, '\\"')}\"`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.killed = false
|
||||
this.handleEvents(stdout, stderr, exit)
|
||||
// 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)
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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'
|
||||
},
|
||||
@@ -68,6 +68,12 @@ const configSchema = {
|
||||
default: true,
|
||||
description: 'Trigger the system\'s beep sound when certain actions cannot be executed or there are no results.'
|
||||
},
|
||||
closeDeletedFileTabs: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
title: 'Close Deleted File Tabs',
|
||||
description: 'Close corresponding editors when a file is deleted outside Atom.'
|
||||
},
|
||||
destroyEmptyPanes: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
@@ -84,45 +90,175 @@ const configSchema = {
|
||||
type: 'string',
|
||||
default: 'utf8',
|
||||
enum: [
|
||||
'cp437',
|
||||
'eucjp',
|
||||
'euckr',
|
||||
'gbk',
|
||||
'iso88591',
|
||||
'iso885910',
|
||||
'iso885913',
|
||||
'iso885914',
|
||||
'iso885915',
|
||||
'iso885916',
|
||||
'iso88592',
|
||||
'iso88593',
|
||||
'iso88594',
|
||||
'iso88595',
|
||||
'iso88596',
|
||||
'iso88597',
|
||||
'iso88597',
|
||||
'iso88598',
|
||||
'koi8r',
|
||||
'koi8u',
|
||||
'macroman',
|
||||
'shiftjis',
|
||||
'utf16be',
|
||||
'utf16le',
|
||||
'utf8',
|
||||
'windows1250',
|
||||
'windows1251',
|
||||
'windows1252',
|
||||
'windows1253',
|
||||
'windows1254',
|
||||
'windows1255',
|
||||
'windows1256',
|
||||
'windows1257',
|
||||
'windows1258',
|
||||
'windows866'
|
||||
{
|
||||
value: 'iso88596',
|
||||
description: 'Arabic (ISO 8859-6)'
|
||||
},
|
||||
{
|
||||
value: 'windows1256',
|
||||
description: 'Arabic (Windows 1256)'
|
||||
},
|
||||
{
|
||||
value: 'iso88594',
|
||||
description: 'Baltic (ISO 8859-4)'
|
||||
},
|
||||
{
|
||||
value: 'windows1257',
|
||||
description: 'Baltic (Windows 1257)'
|
||||
},
|
||||
{
|
||||
value: 'iso885914',
|
||||
description: 'Celtic (ISO 8859-14)'
|
||||
},
|
||||
{
|
||||
value: 'iso88592',
|
||||
description: 'Central European (ISO 8859-2)'
|
||||
},
|
||||
{
|
||||
value: 'windows1250',
|
||||
description: 'Central European (Windows 1250)'
|
||||
},
|
||||
{
|
||||
value: 'gb18030',
|
||||
description: 'Chinese (GB18030)'
|
||||
},
|
||||
{
|
||||
value: 'gbk',
|
||||
description: 'Chinese (GBK)'
|
||||
},
|
||||
{
|
||||
value: 'cp950',
|
||||
description: 'Traditional Chinese (Big5)'
|
||||
},
|
||||
{
|
||||
value: 'big5hkscs',
|
||||
description: 'Traditional Chinese (Big5-HKSCS)'
|
||||
},
|
||||
{
|
||||
value: 'cp866',
|
||||
description: 'Cyrillic (CP 866)'
|
||||
},
|
||||
{
|
||||
value: 'iso88595',
|
||||
description: 'Cyrillic (ISO 8859-5)'
|
||||
},
|
||||
{
|
||||
value: 'koi8r',
|
||||
description: 'Cyrillic (KOI8-R)'
|
||||
},
|
||||
{
|
||||
value: 'koi8u',
|
||||
description: 'Cyrillic (KOI8-U)'
|
||||
},
|
||||
{
|
||||
value: 'windows1251',
|
||||
description: 'Cyrillic (Windows 1251)'
|
||||
},
|
||||
{
|
||||
value: 'cp437',
|
||||
description: 'DOS (CP 437)'
|
||||
},
|
||||
{
|
||||
value: 'cp850',
|
||||
description: 'DOS (CP 850)'
|
||||
},
|
||||
{
|
||||
value: 'iso885913',
|
||||
description: 'Estonian (ISO 8859-13)'
|
||||
},
|
||||
{
|
||||
value: 'iso88597',
|
||||
description: 'Greek (ISO 8859-7)'
|
||||
},
|
||||
{
|
||||
value: 'windows1253',
|
||||
description: 'Greek (Windows 1253)'
|
||||
},
|
||||
{
|
||||
value: 'iso88598',
|
||||
description: 'Hebrew (ISO 8859-8)'
|
||||
},
|
||||
{
|
||||
value: 'windows1255',
|
||||
description: 'Hebrew (Windows 1255)'
|
||||
},
|
||||
{
|
||||
value: 'cp932',
|
||||
description: 'Japanese (CP 932)'
|
||||
},
|
||||
{
|
||||
value: 'eucjp',
|
||||
description: 'Japanese (EUC-JP)'
|
||||
},
|
||||
{
|
||||
value: 'shiftjis',
|
||||
description: 'Japanese (Shift JIS)'
|
||||
},
|
||||
{
|
||||
value: 'euckr',
|
||||
description: 'Korean (EUC-KR)'
|
||||
},
|
||||
{
|
||||
value: 'iso885910',
|
||||
description: 'Nordic (ISO 8859-10)'
|
||||
},
|
||||
{
|
||||
value: 'iso885916',
|
||||
description: 'Romanian (ISO 8859-16)'
|
||||
},
|
||||
{
|
||||
value: 'iso88599',
|
||||
description: 'Turkish (ISO 8859-9)'
|
||||
},
|
||||
{
|
||||
value: 'windows1254',
|
||||
description: 'Turkish (Windows 1254)'
|
||||
},
|
||||
{
|
||||
value: 'utf8',
|
||||
description: 'Unicode (UTF-8)'
|
||||
},
|
||||
{
|
||||
value: 'utf16le',
|
||||
description: 'Unicode (UTF-16 LE)'
|
||||
},
|
||||
{
|
||||
value: 'utf16be',
|
||||
description: 'Unicode (UTF-16 BE)'
|
||||
},
|
||||
{
|
||||
value: 'windows1258',
|
||||
description: 'Vietnamese (Windows 1258)'
|
||||
},
|
||||
{
|
||||
value: 'iso88591',
|
||||
description: 'Western (ISO 8859-1)'
|
||||
},
|
||||
{
|
||||
value: 'iso88593',
|
||||
description: 'Western (ISO 8859-3)'
|
||||
},
|
||||
{
|
||||
value: 'iso885915',
|
||||
description: 'Western (ISO 8859-15)'
|
||||
},
|
||||
{
|
||||
value: 'macroman',
|
||||
description: 'Western (Mac Roman)'
|
||||
},
|
||||
{
|
||||
value: 'windows1252',
|
||||
description: 'Western (Windows 1252)'
|
||||
}
|
||||
]
|
||||
},
|
||||
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
|
||||
},
|
||||
@@ -136,6 +272,12 @@ const configSchema = {
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
useProxySettingsWhenCallingApm: {
|
||||
title: 'Use Proxy Settings When Calling APM',
|
||||
description: 'Use detected proxy settings when calling the `apm` command-line tool.',
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
allowPendingPaneItems: {
|
||||
description: 'Allow items to be previewed without adding them to a pane permanently, such as when single clicking files in the tree view.',
|
||||
type: 'boolean',
|
||||
@@ -164,7 +306,7 @@ const configSchema = {
|
||||
warnOnLargeFileLimit: {
|
||||
description: 'Warn before opening files larger than this number of megabytes.',
|
||||
type: 'number',
|
||||
default: 20
|
||||
default: 40
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -205,6 +347,11 @@ const configSchema = {
|
||||
default: 1.5,
|
||||
description: 'Height of editor lines, as a multiplier of font size.'
|
||||
},
|
||||
showCursorOnSelection: {
|
||||
type: 'boolean',
|
||||
'default': true,
|
||||
description: 'Show cursor while there is a selection.'
|
||||
},
|
||||
showInvisibles: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
|
||||
@@ -336,6 +336,31 @@ ScopeDescriptor = require './scope-descriptor'
|
||||
# order: 2
|
||||
# ```
|
||||
#
|
||||
# ## Manipulating values outside your configuration schema
|
||||
#
|
||||
# It is possible to manipulate(`get`, `set`, `observe` etc) values that do not
|
||||
# appear in your configuration schema. For example, if the config schema of the
|
||||
# package 'some-package' is
|
||||
#
|
||||
# ```coffee
|
||||
# config:
|
||||
# someSetting:
|
||||
# type: 'boolean'
|
||||
# default: false
|
||||
# ```
|
||||
#
|
||||
# You can still do the following
|
||||
#
|
||||
# ```coffee
|
||||
# let otherSetting = atom.config.get('some-package.otherSetting')
|
||||
# atom.config.set('some-package.stillAnotherSetting', otherSetting * 5)
|
||||
# ```
|
||||
#
|
||||
# In other words, if a function asks for a `key-path`, that path doesn't have to
|
||||
# be described in the config schema for the package or any package. However, as
|
||||
# highlighted in the best practices section, you are advised against doing the
|
||||
# above.
|
||||
#
|
||||
# ## Best practices
|
||||
#
|
||||
# * Don't depend on (or write to) configuration keys outside of your keypath.
|
||||
|
||||
@@ -12,15 +12,18 @@ EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g
|
||||
# of a {DisplayMarker}.
|
||||
module.exports =
|
||||
class Cursor extends Model
|
||||
showCursorOnSelection: null
|
||||
screenPosition: null
|
||||
bufferPosition: null
|
||||
goalColumn: null
|
||||
visible: true
|
||||
|
||||
# Instantiated by a {TextEditor}
|
||||
constructor: ({@editor, @marker, id}) ->
|
||||
constructor: ({@editor, @marker, @showCursorOnSelection, id}) ->
|
||||
@emitter = new Emitter
|
||||
|
||||
@showCursorOnSelection ?= true
|
||||
|
||||
@assignId(id)
|
||||
@updateVisibility()
|
||||
|
||||
@@ -575,7 +578,10 @@ class Cursor extends Model
|
||||
isVisible: -> @visible
|
||||
|
||||
updateVisibility: ->
|
||||
@setVisible(@marker.getBufferRange().isEmpty())
|
||||
if @showCursorOnSelection
|
||||
@setVisible(true)
|
||||
else
|
||||
@setVisible(@marker.getBufferRange().isEmpty())
|
||||
|
||||
###
|
||||
Section: Comparing to another cursor
|
||||
@@ -645,6 +651,11 @@ class Cursor extends Model
|
||||
Section: Private
|
||||
###
|
||||
|
||||
setShowCursorOnSelection: (value) ->
|
||||
if value isnt @showCursorOnSelection
|
||||
@showCursorOnSelection = value
|
||||
@updateVisibility()
|
||||
|
||||
getNonWordCharacters: ->
|
||||
@editor.getNonWordCharacters(@getScopeDescriptor().getScopesArray())
|
||||
|
||||
@@ -653,9 +664,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))
|
||||
|
||||
@@ -8,7 +8,7 @@ class DecorationManager extends Model
|
||||
didUpdateDecorationsEventScheduled: false
|
||||
updatedSynchronously: false
|
||||
|
||||
constructor: (@displayLayer, @defaultMarkerLayer) ->
|
||||
constructor: (@displayLayer) ->
|
||||
super
|
||||
|
||||
@emitter = new Emitter
|
||||
@@ -71,9 +71,11 @@ class DecorationManager extends Model
|
||||
|
||||
decorationsForScreenRowRange: (startScreenRow, endScreenRow) ->
|
||||
decorationsByMarkerId = {}
|
||||
for marker in @defaultMarkerLayer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow])
|
||||
if decorations = @decorationsByMarkerId[marker.id]
|
||||
decorationsByMarkerId[marker.id] = decorations
|
||||
for layerId of @decorationCountsByLayerId
|
||||
layer = @displayLayer.getMarkerLayer(layerId)
|
||||
for marker in layer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow])
|
||||
if decorations = @decorationsByMarkerId[marker.id]
|
||||
decorationsByMarkerId[marker.id] = decorations
|
||||
decorationsByMarkerId
|
||||
|
||||
decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) ->
|
||||
@@ -104,7 +106,14 @@ class DecorationManager extends Model
|
||||
decorationsState
|
||||
|
||||
decorateMarker: (marker, decorationParams) ->
|
||||
throw new Error("Cannot decorate a destroyed marker") if marker.isDestroyed()
|
||||
if marker.isDestroyed()
|
||||
error = new Error("Cannot decorate a destroyed marker")
|
||||
error.metadata = {markerLayerIsDestroyed: marker.layer.isDestroyed()}
|
||||
if marker.destroyStackTrace?
|
||||
error.metadata.destroyStackTrace = marker.destroyStackTrace
|
||||
if marker.bufferMarker?.destroyStackTrace?
|
||||
error.metadata.destroyStackTrace = marker.bufferMarker?.destroyStackTrace
|
||||
throw error
|
||||
marker = @displayLayer.getMarkerLayer(marker.layer.id).getMarker(marker.id)
|
||||
decoration = new Decoration(marker, this, decorationParams)
|
||||
@decorationsByMarkerId[marker.id] ?= []
|
||||
@@ -117,6 +126,7 @@ class DecorationManager extends Model
|
||||
decoration
|
||||
|
||||
decorateMarkerLayer: (markerLayer, decorationParams) ->
|
||||
throw new Error("Cannot decorate a destroyed marker layer") if markerLayer.isDestroyed()
|
||||
decoration = new LayerDecoration(markerLayer, this, decorationParams)
|
||||
@layerDecorationsByMarkerLayerId[markerLayer.id] ?= []
|
||||
@layerDecorationsByMarkerLayerId[markerLayer.id].push(decoration)
|
||||
|
||||
@@ -15,7 +15,7 @@ class DefaultDirectoryProvider
|
||||
# * {Directory} if the given URI is compatible with this provider.
|
||||
# * `null` if the given URI is not compatibile with this provider.
|
||||
directoryForURISync: (uri) ->
|
||||
normalizedPath = path.normalize(uri)
|
||||
normalizedPath = @normalizePath(uri)
|
||||
{host} = url.parse(uri)
|
||||
directoryPath = if host
|
||||
uri
|
||||
@@ -42,3 +42,17 @@ class DefaultDirectoryProvider
|
||||
# * `null` if the given URI is not compatibile with this provider.
|
||||
directoryForURI: (uri) ->
|
||||
Promise.resolve(@directoryForURISync(uri))
|
||||
|
||||
# Public: Normalizes path.
|
||||
#
|
||||
# * `uri` {String} The path that should be normalized.
|
||||
#
|
||||
# Returns a {String} with normalized path.
|
||||
normalizePath: (uri) ->
|
||||
# Normalize disk drive letter on Windows to avoid opening two buffers for the same file
|
||||
pathWithNormalizedDiskDriveLetter =
|
||||
if process.platform is 'win32' and matchData = uri.match(/^([a-z]):/)
|
||||
"#{matchData[1].toUpperCase()}#{uri.slice(1)}"
|
||||
else
|
||||
uri
|
||||
path.normalize(pathWithNormalizedDiskDriveLetter)
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
module.exports =
|
||||
class DOMElementPool
|
||||
constructor: ->
|
||||
@freeElementsByTagName = {}
|
||||
@freedElements = new Set
|
||||
|
||||
clear: ->
|
||||
@freedElements.clear()
|
||||
for tagName, freeElements of @freeElementsByTagName
|
||||
freeElements.length = 0
|
||||
return
|
||||
|
||||
build: (tagName, factory, reset) ->
|
||||
element = @freeElementsByTagName[tagName]?.pop()
|
||||
element ?= factory()
|
||||
reset(element)
|
||||
@freedElements.delete(element)
|
||||
element
|
||||
|
||||
buildElement: (tagName, className) ->
|
||||
factory = -> document.createElement(tagName)
|
||||
reset = (element) ->
|
||||
delete element.dataset[dataId] for dataId of element.dataset
|
||||
element.removeAttribute("style")
|
||||
if className?
|
||||
element.className = className
|
||||
else
|
||||
element.removeAttribute("class")
|
||||
@build(tagName, factory, reset)
|
||||
|
||||
buildText: (textContent) ->
|
||||
factory = -> document.createTextNode(textContent)
|
||||
reset = (element) -> element.textContent = textContent
|
||||
@build("#text", factory, reset)
|
||||
|
||||
freeElementAndDescendants: (element) ->
|
||||
@free(element)
|
||||
@freeDescendants(element)
|
||||
|
||||
freeDescendants: (element) ->
|
||||
for descendant in element.childNodes by -1
|
||||
@free(descendant)
|
||||
@freeDescendants(descendant)
|
||||
return
|
||||
|
||||
free: (element) ->
|
||||
throw new Error("The element cannot be null or undefined.") unless element?
|
||||
throw new Error("The element has already been freed!") if @freedElements.has(element)
|
||||
|
||||
tagName = element.nodeName.toLowerCase()
|
||||
@freeElementsByTagName[tagName] ?= []
|
||||
@freeElementsByTagName[tagName].push(element)
|
||||
@freedElements.add(element)
|
||||
|
||||
element.remove()
|
||||
89
src/dom-element-pool.js
Normal file
89
src/dom-element-pool.js
Normal file
@@ -0,0 +1,89 @@
|
||||
module.exports =
|
||||
class DOMElementPool {
|
||||
constructor () {
|
||||
this.managedElements = new Set()
|
||||
this.freeElementsByTagName = new Map()
|
||||
this.freedElements = new Set()
|
||||
}
|
||||
|
||||
clear () {
|
||||
this.managedElements.clear()
|
||||
this.freedElements.clear()
|
||||
this.freeElementsByTagName.clear()
|
||||
}
|
||||
|
||||
buildElement (tagName, className) {
|
||||
const elements = this.freeElementsByTagName.get(tagName)
|
||||
let element = elements ? elements.pop() : null
|
||||
if (element) {
|
||||
for (let dataId in element.dataset) { delete element.dataset[dataId] }
|
||||
element.removeAttribute('style')
|
||||
if (className) {
|
||||
element.className = className
|
||||
} else {
|
||||
element.removeAttribute('class')
|
||||
}
|
||||
while (element.firstChild) {
|
||||
element.removeChild(element.firstChild)
|
||||
}
|
||||
this.freedElements.delete(element)
|
||||
} else {
|
||||
element = document.createElement(tagName)
|
||||
if (className) {
|
||||
element.className = className
|
||||
}
|
||||
this.managedElements.add(element)
|
||||
}
|
||||
return element
|
||||
}
|
||||
|
||||
buildText (textContent) {
|
||||
const elements = this.freeElementsByTagName.get('#text')
|
||||
let element = elements ? elements.pop() : null
|
||||
if (element) {
|
||||
element.textContent = textContent
|
||||
this.freedElements.delete(element)
|
||||
} else {
|
||||
element = document.createTextNode(textContent)
|
||||
this.managedElements.add(element)
|
||||
}
|
||||
return element
|
||||
}
|
||||
|
||||
freeElementAndDescendants (element) {
|
||||
this.free(element)
|
||||
element.remove()
|
||||
}
|
||||
|
||||
freeDescendants (element) {
|
||||
while (element.firstChild) {
|
||||
this.free(element.firstChild)
|
||||
element.removeChild(element.firstChild)
|
||||
}
|
||||
}
|
||||
|
||||
free (element) {
|
||||
if (element == null) { throw new Error('The element cannot be null or undefined.') }
|
||||
if (!this.managedElements.has(element)) return
|
||||
if (this.freedElements.has(element)) {
|
||||
atom.assert(false, 'The element has already been freed!', {
|
||||
content: element instanceof window.Text ? element.textContent : element.outerHTML
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const tagName = element.nodeName.toLowerCase()
|
||||
let elements = this.freeElementsByTagName.get(tagName)
|
||||
if (!elements) {
|
||||
elements = []
|
||||
this.freeElementsByTagName.set(tagName, elements)
|
||||
}
|
||||
elements.push(element)
|
||||
this.freedElements.add(element)
|
||||
|
||||
for (let i = element.childNodes.length - 1; i >= 0; i--) {
|
||||
const descendant = element.childNodes[i]
|
||||
this.free(descendant)
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/get-window-load-settings.js
Normal file
10
src/get-window-load-settings.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const {remote} = require('electron')
|
||||
|
||||
let windowLoadSettings = null
|
||||
|
||||
module.exports = () => {
|
||||
if (!windowLoadSettings) {
|
||||
windowLoadSettings = remote.getCurrentWindow().loadSettings
|
||||
}
|
||||
return windowLoadSettings
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -47,6 +47,8 @@ export class HistoryManager {
|
||||
}
|
||||
|
||||
addProject (paths, lastOpened) {
|
||||
if (paths.length === 0) return
|
||||
|
||||
let project = this.getProject(paths)
|
||||
if (!project) {
|
||||
project = new HistoryProject(paths)
|
||||
@@ -59,10 +61,22 @@ export class HistoryManager {
|
||||
this.didChangeProjects()
|
||||
}
|
||||
|
||||
removeProject (paths) {
|
||||
if (paths.length === 0) return
|
||||
|
||||
let project = this.getProject(paths)
|
||||
if (!project) return
|
||||
|
||||
let index = this.projects.indexOf(project)
|
||||
this.projects.splice(index, 1)
|
||||
|
||||
this.saveState()
|
||||
this.didChangeProjects()
|
||||
}
|
||||
|
||||
getProject (paths) {
|
||||
const pathsString = paths.toString()
|
||||
for (var i = 0; i < this.projects.length; i++) {
|
||||
if (this.projects[i].paths.toString() === pathsString) {
|
||||
if (arrayEquivalent(paths, this.projects[i].paths)) {
|
||||
return this.projects[i]
|
||||
}
|
||||
}
|
||||
@@ -98,6 +112,14 @@ export class HistoryManager {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -86,7 +86,7 @@ module.exports = ->
|
||||
{updateProcessEnv} = require('./update-process-env')
|
||||
path = require 'path'
|
||||
require './window'
|
||||
{getWindowLoadSettings} = require './window-load-settings-helpers'
|
||||
getWindowLoadSettings = require './get-window-load-settings'
|
||||
{ipcRenderer} = require 'electron'
|
||||
{resourcePath, devMode, env} = getWindowLoadSettings()
|
||||
require './electron-shims'
|
||||
|
||||
@@ -6,7 +6,7 @@ import ipcHelpers from './ipc-helpers'
|
||||
import util from 'util'
|
||||
|
||||
export default async function () {
|
||||
const {getWindowLoadSettings} = require('./window-load-settings-helpers')
|
||||
const getWindowLoadSettings = require('./get-window-load-settings')
|
||||
const {test, headless, resourcePath, benchmarkPaths} = getWindowLoadSettings()
|
||||
try {
|
||||
const Clipboard = require('../src/clipboard')
|
||||
|
||||
@@ -18,7 +18,8 @@ module.exports = ({blobStore}) ->
|
||||
try
|
||||
path = require 'path'
|
||||
{ipcRenderer} = require 'electron'
|
||||
{getWindowLoadSettings} = require './window-load-settings-helpers'
|
||||
getWindowLoadSettings = require './get-window-load-settings'
|
||||
CompileCache = require './compile-cache'
|
||||
AtomEnvironment = require '../src/atom-environment'
|
||||
ApplicationDelegate = require '../src/application-delegate'
|
||||
Clipboard = require '../src/clipboard'
|
||||
@@ -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 ?= {}
|
||||
|
||||
@@ -15,6 +15,7 @@ exports.on = function (emitter, eventName, callback) {
|
||||
exports.call = function (channel, ...args) {
|
||||
if (!ipcRenderer) {
|
||||
ipcRenderer = require('electron').ipcRenderer
|
||||
ipcRenderer.setMaxListeners(20)
|
||||
}
|
||||
|
||||
var responseChannel = getResponseChannel(channel)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = []
|
||||
@@ -385,6 +385,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 [
|
||||
@@ -584,8 +587,7 @@ class AtomApplication
|
||||
states = []
|
||||
for window in @windows
|
||||
unless window.isSpec
|
||||
if loadSettings = window.getLoadSettings()
|
||||
states.push(initialPaths: loadSettings.initialPaths)
|
||||
states.push({initialPaths: window.representedDirectoryPaths})
|
||||
if states.length > 0 or allowEmpty
|
||||
@storageFolder.storeSync('application.json', states)
|
||||
|
||||
@@ -796,7 +798,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
|
||||
}
|
||||
}
|
||||
@@ -46,9 +46,7 @@ class AtomWindow
|
||||
if @shouldHideTitleBar()
|
||||
options.titleBarStyle = 'hidden'
|
||||
|
||||
@browserWindow = new BrowserWindow options
|
||||
@atomApplication.addWindow(this)
|
||||
|
||||
@browserWindow = new BrowserWindow(options)
|
||||
@handleEvents()
|
||||
|
||||
loadSettings = Object.assign({}, settings)
|
||||
@@ -60,11 +58,15 @@ class AtomWindow
|
||||
loadSettings.clearWindowState ?= false
|
||||
loadSettings.initialPaths ?=
|
||||
for {pathToOpen} in locationsToOpen when pathToOpen
|
||||
if fs.statSyncNoException(pathToOpen).isFile?()
|
||||
path.dirname(pathToOpen)
|
||||
else
|
||||
stat = fs.statSyncNoException(pathToOpen) or null
|
||||
if stat?.isDirectory()
|
||||
pathToOpen
|
||||
|
||||
else
|
||||
parentDirectory = path.dirname(pathToOpen)
|
||||
if stat?.isFile() or fs.existsSync(parentDirectory)
|
||||
parentDirectory
|
||||
else
|
||||
pathToOpen
|
||||
loadSettings.initialPaths.sort()
|
||||
|
||||
# Only send to the first non-spec window created
|
||||
@@ -72,33 +74,31 @@ class AtomWindow
|
||||
@constructor.includeShellLoadTime = false
|
||||
loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime
|
||||
|
||||
@representedDirectoryPaths = loadSettings.initialPaths
|
||||
@env = loadSettings.env if loadSettings.env?
|
||||
|
||||
@browserWindow.loadSettings = loadSettings
|
||||
|
||||
@browserWindow.on 'window:loaded', =>
|
||||
@emit 'window:loaded'
|
||||
@resolveLoadedPromise()
|
||||
|
||||
@setLoadSettings(loadSettings)
|
||||
@env = loadSettings.env if loadSettings.env?
|
||||
@browserWindow.loadURL url.format
|
||||
protocol: 'file'
|
||||
pathname: "#{@resourcePath}/static/index.html"
|
||||
slashes: true
|
||||
|
||||
@browserWindow.showSaveDialog = @showSaveDialog.bind(this)
|
||||
|
||||
@browserWindow.focusOnWebView() if @isSpec
|
||||
@browserWindow.temporaryState = {windowDimensions} if windowDimensions?
|
||||
|
||||
hasPathToOpen = not (locationsToOpen.length is 1 and not locationsToOpen[0].pathToOpen?)
|
||||
@openLocations(locationsToOpen) if hasPathToOpen and not @isSpecWindow()
|
||||
|
||||
setLoadSettings: (loadSettings) ->
|
||||
@browserWindow.loadURL url.format
|
||||
protocol: 'file'
|
||||
pathname: "#{@resourcePath}/static/index.html"
|
||||
slashes: true
|
||||
hash: encodeURIComponent(JSON.stringify(loadSettings))
|
||||
@atomApplication.addWindow(this)
|
||||
|
||||
getLoadSettings: ->
|
||||
if @browserWindow.webContents? and not @browserWindow.webContents.isLoading()
|
||||
hash = url.parse(@browserWindow.webContents.getURL()).hash.substr(1)
|
||||
JSON.parse(decodeURIComponent(hash))
|
||||
|
||||
hasProjectPath: -> @getLoadSettings().initialPaths?.length > 0
|
||||
hasProjectPath: -> @representedDirectoryPaths.length > 0
|
||||
|
||||
setupContextMenu: ->
|
||||
ContextMenu = require './context-menu'
|
||||
@@ -112,7 +112,7 @@ class AtomWindow
|
||||
true
|
||||
|
||||
containsPath: (pathToCheck) ->
|
||||
@getLoadSettings()?.initialPaths?.some (projectPath) ->
|
||||
@representedDirectoryPaths.some (projectPath) ->
|
||||
if not projectPath
|
||||
false
|
||||
else if not pathToCheck
|
||||
@@ -150,7 +150,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,
|
||||
@@ -262,6 +265,13 @@ class AtomWindow
|
||||
@saveState().then => @browserWindow.reload()
|
||||
@loadedPromise
|
||||
|
||||
showSaveDialog: (params) ->
|
||||
params = Object.assign({
|
||||
title: 'Save File',
|
||||
defaultPath: @representedDirectoryPaths[0]
|
||||
}, params)
|
||||
dialog.showSaveDialog(this, params)
|
||||
|
||||
toggleDevTools: -> @browserWindow.toggleDevTools()
|
||||
|
||||
openDevTools: -> @browserWindow.openDevTools()
|
||||
@@ -272,4 +282,8 @@ class AtomWindow
|
||||
|
||||
setRepresentedFilename: (representedFilename) -> @browserWindow.setRepresentedFilename(representedFilename)
|
||||
|
||||
setRepresentedDirectoryPaths: (@representedDirectoryPaths) ->
|
||||
@representedDirectoryPaths.sort()
|
||||
@atomApplication.saveState()
|
||||
|
||||
copy: -> @browserWindow.copy()
|
||||
|
||||
@@ -22,7 +22,7 @@ class AutoUpdateManager
|
||||
setupAutoUpdater: ->
|
||||
if process.platform is 'win32'
|
||||
archSuffix = if process.arch is 'ia32' then '' else '-' + process.arch
|
||||
@feedUrl = "https://atom.io/api/updates#{archSuffix}"
|
||||
@feedUrl = "https://atom.io/api/updates#{archSuffix}?version=#{@version}"
|
||||
autoUpdater = require './auto-updater-win32'
|
||||
else
|
||||
@feedUrl = "https://atom.io/api/updates?version=#{@version}"
|
||||
|
||||
@@ -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,21 +100,20 @@ 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
|
||||
|
||||
if (args['resource-path']) {
|
||||
devMode = true
|
||||
resourcePath = args['resource-path']
|
||||
devResourcePath = args['resource-path']
|
||||
}
|
||||
|
||||
if (test) {
|
||||
devMode = true
|
||||
}
|
||||
|
||||
if (devMode && !resourcePath) {
|
||||
if (devMode) {
|
||||
resourcePath = devResourcePath
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
@@ -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
|
||||
|
||||
@@ -96,7 +96,7 @@ class PackageTranspilationRegistry {
|
||||
}
|
||||
|
||||
lastPath = thisPath
|
||||
thisPath = path.resolve(thisPath, '..')
|
||||
thisPath = path.join(thisPath, '..')
|
||||
}
|
||||
|
||||
this.specByFilePath[filePath] = null
|
||||
|
||||
@@ -24,6 +24,7 @@ class Package
|
||||
mainModulePath: null
|
||||
resolvedMainModulePath: false
|
||||
mainModule: null
|
||||
mainInitialized: false
|
||||
mainActivated: false
|
||||
|
||||
###
|
||||
@@ -114,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 ?=
|
||||
@@ -140,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)
|
||||
|
||||
@@ -301,6 +321,7 @@ class Package
|
||||
deserialize: (state, atomEnvironment) =>
|
||||
@registerViewProviders()
|
||||
@requireMainModule()
|
||||
@initializeIfNeeded()
|
||||
@mainModule[methodName](state, atomEnvironment)
|
||||
return
|
||||
|
||||
@@ -318,6 +339,7 @@ class Package
|
||||
@requireMainModule()
|
||||
@metadata.viewProviders.forEach (methodName) =>
|
||||
@viewRegistry.addViewProvider (model) =>
|
||||
@initializeIfNeeded()
|
||||
@mainModule[methodName](model)
|
||||
@registeredViewProviders = true
|
||||
|
||||
@@ -420,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'
|
||||
@@ -688,6 +711,9 @@ class Package
|
||||
incompatibleNativeModules
|
||||
|
||||
handleError: (message, error) ->
|
||||
if atom.inSpecMode()
|
||||
throw error
|
||||
|
||||
if error.filename and error.location and (error instanceof SyntaxError)
|
||||
location = "#{error.filename}:#{error.location.first_line + 1}:#{error.location.first_column + 1}"
|
||||
detail = "#{error.message} in #{location}"
|
||||
|
||||
@@ -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: ->
|
||||
|
||||
@@ -21,7 +21,6 @@ class Project extends Model
|
||||
constructor: ({@notificationManager, packageManager, config, @applicationDelegate}) ->
|
||||
@emitter = new Emitter
|
||||
@buffers = []
|
||||
@paths = []
|
||||
@rootDirectories = []
|
||||
@repositories = []
|
||||
@directoryProviders = []
|
||||
@@ -32,7 +31,9 @@ class Project extends Model
|
||||
|
||||
destroyed: ->
|
||||
buffer.destroy() for buffer in @buffers
|
||||
@setPaths([])
|
||||
repository?.destroy() for repository in @repositories
|
||||
@rootDirectories = []
|
||||
@repositories = []
|
||||
|
||||
reset: (packageManager) ->
|
||||
@emitter.dispose()
|
||||
@@ -62,6 +63,9 @@ class Project extends Model
|
||||
fs.closeSync(fs.openSync(bufferState.filePath, 'r'))
|
||||
catch error
|
||||
return unless error.code is 'ENOENT'
|
||||
unless bufferState.shouldDestroyOnFileDelete?
|
||||
bufferState.shouldDestroyOnFileDelete =
|
||||
-> atom.config.get('core.closeDeletedFileTabs')
|
||||
TextBuffer.deserialize(bufferState)
|
||||
|
||||
@subscribeToBuffer(buffer) for buffer in @buffers
|
||||
@@ -70,7 +74,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
|
||||
@@ -197,7 +209,7 @@ class Project extends Model
|
||||
removePath: (projectPath) ->
|
||||
# The projectPath may be a URI, in which case it should not be normalized.
|
||||
unless projectPath in @getPaths()
|
||||
projectPath = path.normalize(projectPath)
|
||||
projectPath = @defaultDirectoryProvider.normalizePath(projectPath)
|
||||
|
||||
indexToRemove = null
|
||||
for directory, i in @rootDirectories
|
||||
@@ -225,11 +237,10 @@ class Project extends Model
|
||||
uri
|
||||
else
|
||||
if fs.isAbsolute(uri)
|
||||
path.normalize(fs.absolute(uri))
|
||||
|
||||
@defaultDirectoryProvider.normalizePath(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)))
|
||||
@defaultDirectoryProvider.normalizePath(fs.resolveHome(path.join(projectPath, uri)))
|
||||
else
|
||||
undefined
|
||||
|
||||
@@ -352,9 +363,14 @@ class Project extends Model
|
||||
else
|
||||
@buildBuffer(absoluteFilePath)
|
||||
|
||||
shouldDestroyBufferOnFileDelete: ->
|
||||
atom.config.get('core.closeDeletedFileTabs')
|
||||
|
||||
# Still needed when deserializing a tokenized buffer
|
||||
buildBufferSync: (absoluteFilePath) ->
|
||||
buffer = new TextBuffer({filePath: absoluteFilePath})
|
||||
buffer = new TextBuffer({
|
||||
filePath: absoluteFilePath
|
||||
shouldDestroyOnFileDelete: @shouldDestroyBufferOnFileDelete})
|
||||
@addBuffer(buffer)
|
||||
buffer.loadSync()
|
||||
buffer
|
||||
@@ -366,7 +382,9 @@ class Project extends Model
|
||||
#
|
||||
# Returns a {Promise} that resolves to the {TextBuffer}.
|
||||
buildBuffer: (absoluteFilePath) ->
|
||||
buffer = new TextBuffer({filePath: absoluteFilePath})
|
||||
buffer = new TextBuffer({
|
||||
filePath: absoluteFilePath
|
||||
shouldDestroyOnFileDelete: @shouldDestroyBufferOnFileDelete})
|
||||
@addBuffer(buffer)
|
||||
buffer.load()
|
||||
.then((buffer) -> buffer)
|
||||
|
||||
@@ -1,59 +1,71 @@
|
||||
/** @babel */
|
||||
|
||||
import { SelectListView } from 'atom-space-pen-views'
|
||||
import SelectListView from 'atom-select-list'
|
||||
|
||||
export default class ReopenProjectListView extends SelectListView {
|
||||
initialize (callback) {
|
||||
export default class ReopenProjectListView {
|
||||
constructor (callback) {
|
||||
this.callback = callback
|
||||
super.initialize()
|
||||
this.addClass('reopen-project')
|
||||
this.list.addClass('mark-active')
|
||||
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')
|
||||
}
|
||||
|
||||
getFilterKey () {
|
||||
return 'name'
|
||||
get element () {
|
||||
return this.selectListView.element
|
||||
}
|
||||
|
||||
destroy () {
|
||||
dispose () {
|
||||
this.cancel()
|
||||
return this.selectListView.destroy()
|
||||
}
|
||||
|
||||
viewForItem (project) {
|
||||
let element = document.createElement('li')
|
||||
if (project.name === this.currentProjectName) {
|
||||
element.classList.add('active')
|
||||
}
|
||||
element.textContent = project.name
|
||||
return element
|
||||
}
|
||||
|
||||
cancelled () {
|
||||
cancel () {
|
||||
if (this.panel != null) {
|
||||
this.panel.destroy()
|
||||
}
|
||||
this.panel = null
|
||||
this.currentProjectName = null
|
||||
}
|
||||
|
||||
confirmed (project) {
|
||||
this.cancel()
|
||||
this.callback(project.value)
|
||||
if (this.previouslyFocusedElement) {
|
||||
this.previouslyFocusedElement.focus()
|
||||
this.previouslyFocusedElement = null
|
||||
}
|
||||
}
|
||||
|
||||
attach () {
|
||||
this.storeFocusedElement()
|
||||
this.previouslyFocusedElement = document.activeElement
|
||||
if (this.panel == null) {
|
||||
this.panel = atom.workspace.addModalPanel({item: this})
|
||||
}
|
||||
this.focusFilterEditor()
|
||||
this.selectListView.focus()
|
||||
this.selectListView.reset()
|
||||
}
|
||||
|
||||
toggle () {
|
||||
async toggle () {
|
||||
if (this.panel != null) {
|
||||
this.cancel()
|
||||
} else {
|
||||
this.currentProjectName = atom.project != null ? this.makeName(atom.project.getPaths()) : null
|
||||
this.setItems(atom.history.getProjects().map(p => ({ name: this.makeName(p.paths), value: p.paths })))
|
||||
const projects = atom.history.getProjects().map(p => ({ name: this.makeName(p.paths), value: p.paths }))
|
||||
await this.selectListView.update({items: projects})
|
||||
this.attach()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ export default class ReopenProjectMenuManager {
|
||||
}),
|
||||
commands.add('atom-workspace', { 'application:reopen-project': this.reopenProjectCommand.bind(this) })
|
||||
)
|
||||
|
||||
this.applyWindowsJumpListRemovals()
|
||||
}
|
||||
|
||||
reopenProjectCommand (e) {
|
||||
@@ -46,6 +48,58 @@ export default class ReopenProjectMenuManager {
|
||||
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()
|
||||
}
|
||||
|
||||
static taskDescription (paths) {
|
||||
return paths.map(path => `${ReopenProjectMenuManager.betterBaseName(path)} (${path})`).join(' ')
|
||||
}
|
||||
|
||||
// Windows users can right-click Atom taskbar and remove project from the jump list.
|
||||
// We have to honor that or the group stops working. As we only get a partial list
|
||||
// each time we remove them from history entirely.
|
||||
applyWindowsJumpListRemovals () {
|
||||
if (process.platform !== 'win32') return
|
||||
if (this.app === undefined) {
|
||||
this.app = require('remote').app
|
||||
}
|
||||
|
||||
const removed = this.app.getJumpListSettings().removedItems.map(i => i.description)
|
||||
if (removed.length === 0) return
|
||||
for (let project of this.historyManager.getProjects()) {
|
||||
if (removed.includes(ReopenProjectMenuManager.taskDescription(project.paths))) {
|
||||
this.historyManager.removeProject(project.paths)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: ReopenProjectMenuManager.taskDescription(project.paths),
|
||||
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 () {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
module.exports =
|
||||
class StateStore {
|
||||
constructor (databaseName, version) {
|
||||
this.connected = false
|
||||
this.dbPromise = new Promise((resolve) => {
|
||||
let dbOpenRequest = indexedDB.open(databaseName, version)
|
||||
dbOpenRequest.onupgradeneeded = (event) => {
|
||||
@@ -10,15 +11,21 @@ class StateStore {
|
||||
db.createObjectStore('states')
|
||||
}
|
||||
dbOpenRequest.onsuccess = () => {
|
||||
this.connected = true
|
||||
resolve(dbOpenRequest.result)
|
||||
}
|
||||
dbOpenRequest.onerror = (error) => {
|
||||
console.error('Could not connect to indexedDB', error)
|
||||
this.connected = false
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
isConnected () {
|
||||
return this.connected
|
||||
}
|
||||
|
||||
connect () {
|
||||
return this.dbPromise.then((db) => !!db)
|
||||
}
|
||||
|
||||
@@ -250,59 +250,70 @@ module.exports = class StyleManager {
|
||||
|
||||
function transformDeprecatedShadowDOMSelectors (css, context) {
|
||||
const transformedSelectors = []
|
||||
const transformedSource = postcss.parse(css)
|
||||
transformedSource.walkRules((rule) => {
|
||||
const transformedSelector = selectorParser((selectors) => {
|
||||
selectors.each((selector) => {
|
||||
const firstNode = selector.nodes[0]
|
||||
if (context === 'atom-text-editor' && firstNode.type === 'pseudo' && firstNode.value === ':host') {
|
||||
const atomTextEditorElementNode = selectorParser.tag({value: 'atom-text-editor'})
|
||||
firstNode.replaceWith(atomTextEditorElementNode)
|
||||
}
|
||||
|
||||
let previousNodeIsAtomTextEditor = false
|
||||
let targetsAtomTextEditorShadow = context === 'atom-text-editor'
|
||||
let previousNode
|
||||
selector.each((node) => {
|
||||
if (targetsAtomTextEditorShadow && node.type === 'class') {
|
||||
if (DEPRECATED_SYNTAX_SELECTORS.has(node.value)) {
|
||||
node.value = `syntax--${node.value}`
|
||||
}
|
||||
} else {
|
||||
if (previousNodeIsAtomTextEditor && node.type === 'pseudo' && node.value === '::shadow') {
|
||||
node.type = 'className'
|
||||
node.value = '.editor'
|
||||
targetsAtomTextEditorShadow = true
|
||||
}
|
||||
}
|
||||
|
||||
previousNode = node
|
||||
if (node.type === 'combinator') {
|
||||
previousNodeIsAtomTextEditor = false
|
||||
} else if (previousNode.type === 'tag' && previousNode.value === 'atom-text-editor') {
|
||||
previousNodeIsAtomTextEditor = true
|
||||
}
|
||||
})
|
||||
})
|
||||
}).process(rule.selector, {lossless: true}).result
|
||||
if (transformedSelector !== rule.selector) {
|
||||
transformedSelectors.push({before: rule.selector, after: transformedSelector})
|
||||
rule.selector = transformedSelector
|
||||
}
|
||||
})
|
||||
let deprecationMessage
|
||||
if (transformedSelectors.length > 0) {
|
||||
deprecationMessage = 'Starting from Atom v1.13.0, the contents of `atom-text-editor` elements '
|
||||
deprecationMessage += 'are no longer encapsulated within a shadow DOM boundary. '
|
||||
deprecationMessage += 'This means you should stop using `:host` and `::shadow` '
|
||||
deprecationMessage += 'pseudo-selectors, and prepend all your syntax selectors with `syntax--`. '
|
||||
deprecationMessage += 'To prevent breakage with existing style sheets, Atom will automatically '
|
||||
deprecationMessage += 'upgrade the following selectors:\n\n'
|
||||
deprecationMessage += transformedSelectors
|
||||
.map((selector) => `* \`${selector.before}\` => \`${selector.after}\``)
|
||||
.join('\n\n') + '\n\n'
|
||||
deprecationMessage += 'Automatic translation of selectors will be removed in a few release cycles to minimize startup time. '
|
||||
deprecationMessage += 'Please, make sure to upgrade the above selectors as soon as possible.'
|
||||
let transformedSource
|
||||
try {
|
||||
transformedSource = postcss.parse(css)
|
||||
} catch (e) {
|
||||
transformedSource = null
|
||||
}
|
||||
|
||||
if (transformedSource) {
|
||||
transformedSource.walkRules((rule) => {
|
||||
const transformedSelector = selectorParser((selectors) => {
|
||||
selectors.each((selector) => {
|
||||
const firstNode = selector.nodes[0]
|
||||
if (context === 'atom-text-editor' && firstNode.type === 'pseudo' && firstNode.value === ':host') {
|
||||
const atomTextEditorElementNode = selectorParser.tag({value: 'atom-text-editor'})
|
||||
firstNode.replaceWith(atomTextEditorElementNode)
|
||||
}
|
||||
|
||||
let previousNodeIsAtomTextEditor = false
|
||||
let targetsAtomTextEditorShadow = context === 'atom-text-editor'
|
||||
let previousNode
|
||||
selector.each((node) => {
|
||||
if (targetsAtomTextEditorShadow && node.type === 'class') {
|
||||
if (DEPRECATED_SYNTAX_SELECTORS.has(node.value)) {
|
||||
node.value = `syntax--${node.value}`
|
||||
}
|
||||
} else {
|
||||
if (previousNodeIsAtomTextEditor && node.type === 'pseudo' && node.value === '::shadow') {
|
||||
node.type = 'className'
|
||||
node.value = '.editor'
|
||||
targetsAtomTextEditorShadow = true
|
||||
}
|
||||
}
|
||||
|
||||
previousNode = node
|
||||
if (node.type === 'combinator') {
|
||||
previousNodeIsAtomTextEditor = false
|
||||
} else if (previousNode.type === 'tag' && previousNode.value === 'atom-text-editor') {
|
||||
previousNodeIsAtomTextEditor = true
|
||||
}
|
||||
})
|
||||
})
|
||||
}).process(rule.selector, {lossless: true}).result
|
||||
if (transformedSelector !== rule.selector) {
|
||||
transformedSelectors.push({before: rule.selector, after: transformedSelector})
|
||||
rule.selector = transformedSelector
|
||||
}
|
||||
})
|
||||
let deprecationMessage
|
||||
if (transformedSelectors.length > 0) {
|
||||
deprecationMessage = 'Starting from Atom v1.13.0, the contents of `atom-text-editor` elements '
|
||||
deprecationMessage += 'are no longer encapsulated within a shadow DOM boundary. '
|
||||
deprecationMessage += 'This means you should stop using `:host` and `::shadow` '
|
||||
deprecationMessage += 'pseudo-selectors, and prepend all your syntax selectors with `syntax--`. '
|
||||
deprecationMessage += 'To prevent breakage with existing style sheets, Atom will automatically '
|
||||
deprecationMessage += 'upgrade the following selectors:\n\n'
|
||||
deprecationMessage += transformedSelectors
|
||||
.map((selector) => `* \`${selector.before}\` => \`${selector.after}\``)
|
||||
.join('\n\n') + '\n\n'
|
||||
deprecationMessage += 'Automatic translation of selectors will be removed in a few release cycles to minimize startup time. '
|
||||
deprecationMessage += 'Please, make sure to upgrade the above selectors as soon as possible.'
|
||||
}
|
||||
return {source: transformedSource.toString(), deprecationMessage}
|
||||
} else {
|
||||
// CSS was malformed so we don't transform it.
|
||||
return {source: css}
|
||||
}
|
||||
return {source: transformedSource.toString(), deprecationMessage}
|
||||
}
|
||||
|
||||
@@ -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,12 +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.
|
||||
@hiddenInputComponent.getDomNode().getModel = => @editor
|
||||
hiddenInputElement.getModel = => @editor
|
||||
|
||||
@linesComponent = new LinesComponent({@presenter, @domElementPool, @assert, @grammars, @views})
|
||||
@scrollViewNode.appendChild(@linesComponent.getDomNode())
|
||||
@@ -346,7 +346,6 @@ class TextEditorComponent
|
||||
focused: ->
|
||||
if @mounted
|
||||
@presenter.setFocused(true)
|
||||
@hiddenInputComponent.getDomNode().focus()
|
||||
|
||||
blurred: ->
|
||||
if @mounted
|
||||
@@ -420,7 +419,6 @@ class TextEditorComponent
|
||||
|
||||
onScrollViewScroll: =>
|
||||
if @mounted
|
||||
console.warn "TextEditorScrollView scrolled when it shouldn't have."
|
||||
@scrollViewNode.scrollTop = 0
|
||||
@scrollViewNode.scrollLeft = 0
|
||||
|
||||
@@ -616,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)
|
||||
|
||||
@@ -909,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)
|
||||
@@ -99,7 +108,10 @@ class TextEditorElement extends HTMLElement
|
||||
|
||||
buildModel: ->
|
||||
@setModel(@workspace.buildTextEditor(
|
||||
buffer: new TextBuffer(@textContent)
|
||||
buffer: new TextBuffer({
|
||||
text: @textContent
|
||||
shouldDestroyOnFileDelete:
|
||||
-> atom.config.get('core.closeDeletedFileTabs')})
|
||||
softWrapped: false
|
||||
tabLength: 2
|
||||
softTabs: true
|
||||
@@ -117,12 +129,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 +142,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, ' ')
|
||||
|
||||
@@ -451,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
|
||||
@@ -466,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
|
||||
@@ -493,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)"
|
||||
@@ -596,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)
|
||||
@@ -617,6 +622,18 @@ class TextEditorPresenter
|
||||
return unless @scrollTop? and @lineHeight?
|
||||
|
||||
@startRow = Math.max(0, @lineTopIndex.rowForPixelPosition(@scrollTop))
|
||||
atom.assert(
|
||||
Number.isFinite(@startRow),
|
||||
'Invalid start row',
|
||||
(error) =>
|
||||
error.metadata = {
|
||||
startRow: @startRow?.toString(),
|
||||
scrollTop: @scrollTop?.toString(),
|
||||
scrollHeight: @scrollHeight?.toString(),
|
||||
clientHeight: @clientHeight?.toString(),
|
||||
lineHeight: @lineHeight?.toString()
|
||||
}
|
||||
)
|
||||
|
||||
updateEndRow: ->
|
||||
return unless @scrollTop? and @lineHeight? and @height?
|
||||
@@ -1001,8 +1018,7 @@ class TextEditorPresenter
|
||||
@lineHeight? and @baseCharacterWidth?
|
||||
|
||||
pixelPositionForScreenPosition: (screenPosition) ->
|
||||
position =
|
||||
@linesYardstick.pixelPositionForScreenPosition(screenPosition)
|
||||
position = @linesYardstick.pixelPositionForScreenPosition(screenPosition)
|
||||
position.top -= @getScrollTop()
|
||||
position.left -= @getScrollLeft()
|
||||
|
||||
@@ -1140,7 +1156,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
|
||||
|
||||
@@ -1225,13 +1243,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 = []
|
||||
@@ -1408,11 +1427,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
|
||||
|
||||
@@ -11,6 +11,7 @@ const EDITOR_PARAMS_BY_SETTING_KEY = [
|
||||
['editor.showInvisibles', 'showInvisibles'],
|
||||
['editor.tabLength', 'tabLength'],
|
||||
['editor.invisibles', 'invisibles'],
|
||||
['editor.showCursorOnSelection', 'showCursorOnSelection'],
|
||||
['editor.showIndentGuide', 'showIndentGuide'],
|
||||
['editor.showLineNumbers', 'showLineNumbers'],
|
||||
['editor.softWrap', 'softWrapped'],
|
||||
|
||||
@@ -16,12 +16,11 @@ TextEditorElement = require './text-editor-element'
|
||||
{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils'
|
||||
|
||||
ZERO_WIDTH_NBSP = '\ufeff'
|
||||
MAX_SCREEN_LINE_LENGTH = 500
|
||||
|
||||
# Essential: This class represents all essential editing state for a single
|
||||
# {TextBuffer}, including cursor and selection positions, folds, and soft wraps.
|
||||
# If you're manipulating the state of an editor, use this class. If you're
|
||||
# interested in the visual appearance of editors, use {TextEditorElement}
|
||||
# instead.
|
||||
# If you're manipulating the state of an editor, use this class.
|
||||
#
|
||||
# A single {TextBuffer} can belong to multiple editors. For example, if the
|
||||
# same file is open in two different panes, Atom creates a separate editor for
|
||||
@@ -67,6 +66,7 @@ class TextEditor extends Model
|
||||
buffer: null
|
||||
languageMode: null
|
||||
cursors: null
|
||||
showCursorOnSelection: null
|
||||
selections: null
|
||||
suppressSelectionMerging: false
|
||||
selectionFlashDuration: 500
|
||||
@@ -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
|
||||
@@ -136,7 +133,8 @@ class TextEditor extends Model
|
||||
@mini, @placeholderText, lineNumberGutterVisible, @largeFileMode,
|
||||
@assert, grammar, @showInvisibles, @autoHeight, @autoWidth, @scrollPastEnd, @editorWidthInChars,
|
||||
@tokenizedBuffer, @displayLayer, @invisibles, @showIndentGuide,
|
||||
@softWrapped, @softWrapAtPreferredLineLength, @preferredLineLength
|
||||
@softWrapped, @softWrapAtPreferredLineLength, @preferredLineLength,
|
||||
@showCursorOnSelection
|
||||
} = params
|
||||
|
||||
@assert ?= (condition) -> condition
|
||||
@@ -156,33 +154,37 @@ class TextEditor extends Model
|
||||
tabLength ?= 2
|
||||
@autoIndent ?= true
|
||||
@autoIndentOnPaste ?= true
|
||||
@showCursorOnSelection ?= true
|
||||
@undoGroupingInterval ?= 300
|
||||
@nonWordCharacters ?= "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-…"
|
||||
@softWrapped ?= false
|
||||
@softWrapAtPreferredLineLength ?= false
|
||||
@preferredLineLength ?= 80
|
||||
|
||||
@buffer ?= new TextBuffer
|
||||
@buffer ?= new TextBuffer({shouldDestroyOnFileDelete: ->
|
||||
atom.config.get('core.closeDeletedFileTabs')})
|
||||
@tokenizedBuffer ?= new TokenizedBuffer({
|
||||
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 =>
|
||||
@@ -191,8 +193,9 @@ class TextEditor extends Model
|
||||
@displayLayer.setTextDecorationLayer(@tokenizedBuffer)
|
||||
@defaultMarkerLayer = @displayLayer.addMarkerLayer()
|
||||
@selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true, persistent: true)
|
||||
@selectionsMarkerLayer.trackDestructionInOnDidCreateMarkerCallbacks = true
|
||||
|
||||
@decorationManager = new DecorationManager(@displayLayer, @defaultMarkerLayer)
|
||||
@decorationManager = new DecorationManager(@displayLayer)
|
||||
@decorateMarkerLayer(@displayLayer.foldsMarkerLayer, {type: 'line-number', class: 'folded'})
|
||||
|
||||
for marker in @selectionsMarkerLayer.getMarkers()
|
||||
@@ -222,7 +225,6 @@ class TextEditor extends Model
|
||||
@backgroundWorkHandle = null
|
||||
|
||||
update: (params) ->
|
||||
currentSoftWrapColumn = @getSoftWrapColumn()
|
||||
displayLayerParams = {}
|
||||
|
||||
for param in Object.keys(params)
|
||||
@@ -273,16 +275,12 @@ class TextEditor extends Model
|
||||
when 'softWrapAtPreferredLineLength'
|
||||
if value isnt @softWrapAtPreferredLineLength
|
||||
@softWrapAtPreferredLineLength = value
|
||||
softWrapColumn = @getSoftWrapColumn()
|
||||
if softWrapColumn isnt currentSoftWrapColumn
|
||||
displayLayerParams.softWrapColumn = softWrapColumn
|
||||
displayLayerParams.softWrapColumn = @getSoftWrapColumn()
|
||||
|
||||
when 'preferredLineLength'
|
||||
if value isnt @preferredLineLength
|
||||
@preferredLineLength = value
|
||||
softWrapColumn = @getSoftWrapColumn()
|
||||
if softWrapColumn isnt currentSoftWrapColumn
|
||||
displayLayerParams.softWrapColumn = softWrapColumn
|
||||
displayLayerParams.softWrapColumn = @getSoftWrapColumn()
|
||||
|
||||
when 'mini'
|
||||
if value isnt @mini
|
||||
@@ -327,16 +325,12 @@ class TextEditor extends Model
|
||||
when 'editorWidthInChars'
|
||||
if value > 0 and value isnt @editorWidthInChars
|
||||
@editorWidthInChars = value
|
||||
softWrapColumn = @getSoftWrapColumn()
|
||||
if softWrapColumn isnt currentSoftWrapColumn
|
||||
displayLayerParams.softWrapColumn = softWrapColumn
|
||||
displayLayerParams.softWrapColumn = @getSoftWrapColumn()
|
||||
|
||||
when 'width'
|
||||
if value isnt @width
|
||||
@width = value
|
||||
softWrapColumn = @getSoftWrapColumn()
|
||||
if softWrapColumn isnt currentSoftWrapColumn
|
||||
displayLayerParams.softWrapColumn = softWrapColumn
|
||||
displayLayerParams.softWrapColumn = @getSoftWrapColumn()
|
||||
|
||||
when 'scrollPastEnd'
|
||||
if value isnt @scrollPastEnd
|
||||
@@ -352,11 +346,16 @@ class TextEditor extends Model
|
||||
if value isnt @autoWidth
|
||||
@autoWidth = value
|
||||
@presenter?.didChangeAutoWidth()
|
||||
|
||||
when 'showCursorOnSelection'
|
||||
if value isnt @showCursorOnSelection
|
||||
@showCursorOnSelection = value
|
||||
cursor.setShowCursorOnSelection(value) for cursor in @getCursors()
|
||||
|
||||
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()
|
||||
@@ -421,14 +420,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
|
||||
@@ -732,7 +732,7 @@ class TextEditor extends Model
|
||||
tabLength: @tokenizedBuffer.getTabLength(),
|
||||
@firstVisibleScreenRow, @firstVisibleScreenColumn,
|
||||
@assert, displayLayer, grammar: @getGrammar(),
|
||||
@autoWidth, @autoHeight
|
||||
@autoWidth, @autoHeight, @showCursorOnSelection
|
||||
})
|
||||
|
||||
# Controls visibility based on the given {Boolean}.
|
||||
@@ -902,7 +902,7 @@ class TextEditor extends Model
|
||||
# Determine whether the user should be prompted to save before closing
|
||||
# this editor.
|
||||
shouldPromptToSave: ({windowCloseRequested, projectHasPaths}={}) ->
|
||||
if windowCloseRequested and projectHasPaths
|
||||
if windowCloseRequested and projectHasPaths and atom.stateStore.isConnected()
|
||||
false
|
||||
else
|
||||
@isModified() and not @buffer.hasMultipleEditors()
|
||||
@@ -991,10 +991,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()
|
||||
|
||||
@@ -1085,8 +1082,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.
|
||||
@@ -1143,13 +1140,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.
|
||||
@@ -1205,15 +1202,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.
|
||||
@@ -1285,30 +1282,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
|
||||
@@ -1749,10 +1760,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) ->
|
||||
@@ -2264,13 +2279,12 @@ class TextEditor extends Model
|
||||
|
||||
# Add a cursor based on the given {DisplayMarker}.
|
||||
addCursor: (marker) ->
|
||||
cursor = new Cursor(editor: this, marker: marker)
|
||||
cursor = new Cursor(editor: this, marker: marker, showCursorOnSelection: @showCursorOnSelection)
|
||||
@cursors.push(cursor)
|
||||
@cursorsByMarkerId.set(marker.id, cursor)
|
||||
@decorateMarker(marker, type: 'line-number', class: 'cursor-line')
|
||||
@decorateMarker(marker, type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true)
|
||||
@decorateMarker(marker, type: 'line', class: 'cursor-line', onlyEmpty: true)
|
||||
@emitter.emit 'did-add-cursor', cursor
|
||||
cursor
|
||||
|
||||
moveCursors: (fn) ->
|
||||
@@ -2759,6 +2773,7 @@ class TextEditor extends Model
|
||||
if selection.intersectsBufferRange(selectionBufferRange)
|
||||
return selection
|
||||
else
|
||||
@emitter.emit 'did-add-cursor', cursor
|
||||
@emitter.emit 'did-add-selection', selection
|
||||
selection
|
||||
|
||||
@@ -2925,11 +2940,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.
|
||||
#
|
||||
@@ -2955,7 +2966,7 @@ class TextEditor extends Model
|
||||
else
|
||||
@getEditorWidthInChars()
|
||||
else
|
||||
Infinity
|
||||
MAX_SCREEN_LINE_LENGTH
|
||||
|
||||
###
|
||||
Section: Indentation
|
||||
@@ -3465,6 +3476,11 @@ class TextEditor extends Model
|
||||
# Returns a positive {Number}.
|
||||
getScrollSensitivity: -> @scrollSensitivity
|
||||
|
||||
# Experimental: Does this editor show cursors while there is a selection?
|
||||
#
|
||||
# Returns a positive {Boolean}.
|
||||
getShowCursorOnSelection: -> @showCursorOnSelection
|
||||
|
||||
# Experimental: Are line numbers enabled for this editor?
|
||||
#
|
||||
# Returns a {Boolean}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -8,6 +8,8 @@ ScopeDescriptor = require './scope-descriptor'
|
||||
TokenizedBufferIterator = require './tokenized-buffer-iterator'
|
||||
NullGrammar = require './null-grammar'
|
||||
|
||||
MAX_LINE_LENGTH_TO_TOKENIZE = 500
|
||||
|
||||
module.exports =
|
||||
class TokenizedBuffer extends Model
|
||||
grammar: null
|
||||
@@ -41,6 +43,7 @@ class TokenizedBuffer extends Model
|
||||
|
||||
destroyed: ->
|
||||
@disposables.dispose()
|
||||
@tokenizedLines.length = 0
|
||||
|
||||
buildIterator: ->
|
||||
new TokenizedBufferIterator(this)
|
||||
@@ -94,6 +97,7 @@ class TokenizedBuffer extends Model
|
||||
false
|
||||
|
||||
retokenizeLines: ->
|
||||
return unless @alive
|
||||
@fullyTokenized = false
|
||||
@tokenizedLines = new Array(@buffer.getLineCount())
|
||||
@invalidRows = []
|
||||
@@ -198,10 +202,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.
|
||||
@@ -252,6 +253,8 @@ class TokenizedBuffer extends Model
|
||||
|
||||
buildTokenizedLineForRowWithText: (row, text, ruleStack = @stackForRow(row - 1), openScopes = @openScopesForRow(row)) ->
|
||||
lineEnding = @buffer.lineEndingForRow(row)
|
||||
if text.length > MAX_LINE_LENGTH_TO_TOKENIZE
|
||||
text = text.slice(0, MAX_LINE_LENGTH_TO_TOKENIZE)
|
||||
{tags, ruleStack} = @grammar.tokenizeLine(text, ruleStack, row is 0, false)
|
||||
new TokenizedLine({openScopes, text, tags, ruleStack, lineEnding, @tokenIterator})
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -62,17 +62,32 @@ function shouldGetEnvFromShell (env) {
|
||||
|
||||
async function getEnvFromShell (env) {
|
||||
let {stdout, error} = await new Promise((resolve) => {
|
||||
let child
|
||||
let error
|
||||
let stdout = ''
|
||||
const child = childProcess.spawn(env.SHELL, ['-ilc', 'command env'], {encoding: 'utf8', stdio: ['ignore', 'pipe', process.stderr]})
|
||||
let done = false
|
||||
const cleanup = () => {
|
||||
if (!done && child) {
|
||||
child.kill()
|
||||
done = true
|
||||
}
|
||||
}
|
||||
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')
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
windowLoadSettings = null
|
||||
|
||||
exports.getWindowLoadSettings = ->
|
||||
windowLoadSettings ?= JSON.parse(window.decodeURIComponent(window.location.hash.substr(1)))
|
||||
|
||||
exports.setWindowLoadSettings = (settings) ->
|
||||
windowLoadSettings = settings
|
||||
location.hash = encodeURIComponent(JSON.stringify(settings))
|
||||
@@ -102,7 +102,7 @@ class WorkspaceElement extends HTMLElement
|
||||
getModel: -> @model
|
||||
|
||||
handleMousewheel: (event) ->
|
||||
if event.ctrlKey and @config.get('editor.zoomFontWhenCtrlScrolling') and event.target.matches('atom-text-editor')
|
||||
if event.ctrlKey and @config.get('editor.zoomFontWhenCtrlScrolling') and event.target.closest('atom-text-editor')?
|
||||
if event.wheelDeltaY > 0
|
||||
@model.increaseFontSize()
|
||||
else if event.wheelDeltaY < 0
|
||||
|
||||
@@ -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 projectPaths[0]
|
||||
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