Merge branch 'master' into as-ns-startup-snapshot

This commit is contained in:
Antonio Scandurra
2017-02-23 10:01:52 +01:00
141 changed files with 2808 additions and 1418 deletions

View File

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

View File

@@ -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
View File

@@ -0,0 +1,62 @@
/** @babel */
const fs = require('fs-plus')
const path = require('path')
const hasWriteAccess = (dir) => {
const testFilePath = path.join(dir, 'write.test')
try {
fs.writeFileSync(testFilePath, new Date().toISOString(), { flag: 'w+' })
fs.unlinkSync(testFilePath)
return true
} catch (err) {
return false
}
}
const getAppDirectory = () => {
switch (process.platform) {
case 'darwin':
return 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
}

View File

@@ -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
}

View File

@@ -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)
}
/*

View File

@@ -12,7 +12,7 @@ const configSchema = {
properties: {
ignoredNames: {
type: 'array',
default: ['.git', '.hg', '.svn', '.DS_Store', '._*', 'Thumbs.db'],
default: ['.git', '.hg', '.svn', '.DS_Store', '._*', 'Thumbs.db', 'desktop.ini'],
items: {
type: 'string'
},
@@ -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,

View File

@@ -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.

View File

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

View File

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

View File

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

View File

@@ -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
View 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)
}
}
}

View File

@@ -0,0 +1,10 @@
const {remote} = require('electron')
let windowLoadSettings = null
module.exports = () => {
if (!windowLoadSettings) {
windowLoadSettings = remote.getCurrentWindow().loadSettings
}
return windowLoadSettings
}

View File

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

View File

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

View File

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

View File

@@ -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'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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?

View File

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

View File

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

View File

@@ -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}"

View File

@@ -41,10 +41,6 @@ module.exports = function parseCommandLine (processArgs) {
'safe',
'Do not load packages from ~/.atom/packages or ~/.atom/dev/packages.'
)
options.boolean('portable').describe(
'portable',
'Set portable mode. Copies the ~/.atom folder to be a sibling of the installed Atom location if a .atom folder is not already there.'
)
options.boolean('benchmark').describe('benchmark', 'Open a new window that runs the specified benchmarks.')
options.boolean('benchmark-test').describe('benchmark--test', 'Run a faster version of the benchmarks in headless mode.')
options.alias('t', 'test').boolean('t').describe('t', 'Run the specified specs and exit with error code on failures.')
@@ -104,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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -96,7 +96,7 @@ class PackageTranspilationRegistry {
}
lastPath = thisPath
thisPath = path.resolve(thisPath, '..')
thisPath = path.join(thisPath, '..')
}
this.specByFilePath[filePath] = null

View File

@@ -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}"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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}
}

View File

@@ -42,7 +42,7 @@ class TextEditorComponent
@assert domNode?, "TextEditorComponent::domNode was set to null."
@domNodeValue = domNode
constructor: ({@editor, @hostElement, tileSize, @views, @themes, @styles, @assert}) ->
constructor: ({@editor, @hostElement, tileSize, @views, @themes, @styles, @assert, hiddenInputElement}) ->
@tileSize = tileSize if tileSize?
@disposables = new CompositeDisposable
@@ -70,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

View File

@@ -25,8 +25,17 @@ class TextEditorElement extends HTMLElement
@emitter = new Emitter
@subscriptions = new CompositeDisposable
@hiddenInputElement = document.createElement('input')
@hiddenInputElement.classList.add('hidden-input')
@hiddenInputElement.setAttribute('tabindex', -1)
@hiddenInputElement.setAttribute('data-react-skip-selection-restoration', true)
@hiddenInputElement.style['-webkit-transform'] = 'translateZ(0)'
@hiddenInputElement.addEventListener 'paste', (event) -> event.preventDefault()
@addEventListener 'focus', @focused.bind(this)
@addEventListener 'blur', @blurred.bind(this)
@hiddenInputElement.addEventListener 'focus', @focused.bind(this)
@hiddenInputElement.addEventListener 'blur', @inputNodeBlurred.bind(this)
@classList.add('editor')
@setAttribute('tabindex', -1)
@@ -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, ' ')

View File

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

View File

@@ -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'],

View File

@@ -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}

View File

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

View File

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

View File

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

View File

@@ -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')
}

View File

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

View File

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

View File

@@ -182,7 +182,7 @@ class Workspace extends Model
projectPath = _.find projectPaths, (projectPath) ->
itemPath is projectPath or itemPath?.startsWith(projectPath + path.sep)
itemTitle ?= "untitled"
projectPath ?= projectPaths[0]
projectPath ?= if itemPath then path.dirname(itemPath) else 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