Merge branch 'master' into ns-use-display-layers

# Conflicts:
#	package.json
#	src/display-buffer.coffee
#	src/text-editor.coffee
#	src/tokenized-buffer.coffee
This commit is contained in:
Antonio Scandurra
2016-03-10 13:53:14 +01:00
114 changed files with 2475 additions and 1047 deletions

View File

@@ -1,69 +1,73 @@
_ = require 'underscore-plus'
ipc = require 'ipc'
remote = require 'remote'
shell = require 'shell'
webFrame = require 'web-frame'
{ipcRenderer, remote, shell, webFrame} = require 'electron'
ipcHelpers = require './ipc-helpers'
{Disposable} = require 'event-kit'
{getWindowLoadSettings, setWindowLoadSettings} = require './window-load-settings-helpers'
module.exports =
class ApplicationDelegate
open: (params) ->
ipc.send('open', params)
ipcRenderer.send('open', params)
pickFolder: (callback) ->
responseChannel = "atom-pick-folder-response"
ipc.on responseChannel, (path) ->
ipc.removeAllListeners(responseChannel)
ipcRenderer.on responseChannel, (event, path) ->
ipcRenderer.removeAllListeners(responseChannel)
callback(path)
ipc.send("pick-folder", responseChannel)
ipcRenderer.send("pick-folder", responseChannel)
getCurrentWindow: ->
remote.getCurrentWindow()
closeWindow: ->
ipc.send("call-window-method", "close")
ipcRenderer.send("call-window-method", "close")
getTemporaryWindowState: ->
ipcHelpers.call('get-temporary-window-state')
setTemporaryWindowState: (state) ->
ipcHelpers.call('set-temporary-window-state', state)
getWindowSize: ->
[width, height] = remote.getCurrentWindow().getSize()
{width, height}
setWindowSize: (width, height) ->
remote.getCurrentWindow().setSize(width, height)
ipcHelpers.call('set-window-size', width, height)
getWindowPosition: ->
[x, y] = remote.getCurrentWindow().getPosition()
{x, y}
setWindowPosition: (x, y) ->
ipc.send("call-window-method", "setPosition", x, y)
ipcHelpers.call('set-window-position', x, y)
centerWindow: ->
ipc.send("call-window-method", "center")
ipcHelpers.call('center-window')
focusWindow: ->
ipc.send("call-window-method", "focus")
ipcHelpers.call('focus-window')
showWindow: ->
ipc.send("call-window-method", "show")
ipcHelpers.call('show-window')
hideWindow: ->
ipc.send("call-window-method", "hide")
ipcHelpers.call('hide-window')
restartWindow: ->
ipc.send("call-window-method", "restart")
reloadWindow: ->
ipcRenderer.send("call-window-method", "reload")
isWindowMaximized: ->
remote.getCurrentWindow().isMaximized()
maximizeWindow: ->
ipc.send("call-window-method", "maximize")
ipcRenderer.send("call-window-method", "maximize")
isWindowFullScreen: ->
remote.getCurrentWindow().isFullScreen()
setWindowFullScreen: (fullScreen=false) ->
ipc.send("call-window-method", "setFullScreen", fullScreen)
ipcRenderer.send("call-window-method", "setFullScreen", fullScreen)
openWindowDevTools: ->
new Promise (resolve) ->
@@ -75,7 +79,7 @@ class ApplicationDelegate
resolve()
else
remote.getCurrentWindow().once("devtools-opened", -> resolve())
ipc.send("call-window-method", "openDevTools")
ipcRenderer.send("call-window-method", "openDevTools")
closeWindowDevTools: ->
new Promise (resolve) ->
@@ -87,7 +91,7 @@ class ApplicationDelegate
resolve()
else
remote.getCurrentWindow().once("devtools-closed", -> resolve())
ipc.send("call-window-method", "closeDevTools")
ipcRenderer.send("call-window-method", "closeDevTools")
toggleWindowDevTools: ->
new Promise (resolve) =>
@@ -101,16 +105,16 @@ class ApplicationDelegate
@openWindowDevTools().then(resolve)
executeJavaScriptInWindowDevTools: (code) ->
ipc.send("call-window-method", "executeJavaScriptInDevTools", code)
ipcRenderer.send("execute-javascript-in-dev-tools", code)
setWindowDocumentEdited: (edited) ->
ipc.send("call-window-method", "setDocumentEdited", edited)
ipcRenderer.send("call-window-method", "setDocumentEdited", edited)
setRepresentedFilename: (filename) ->
ipc.send("call-window-method", "setRepresentedFilename", filename)
ipcRenderer.send("call-window-method", "setRepresentedFilename", filename)
addRecentDocument: (filename) ->
ipc.send("add-recent-document", filename)
ipcRenderer.send("add-recent-document", filename)
setRepresentedDirectoryPaths: (paths) ->
loadSettings = getWindowLoadSettings()
@@ -118,14 +122,13 @@ class ApplicationDelegate
setWindowLoadSettings(loadSettings)
setAutoHideWindowMenuBar: (autoHide) ->
ipc.send("call-window-method", "setAutoHideMenuBar", autoHide)
ipcRenderer.send("call-window-method", "setAutoHideMenuBar", autoHide)
setWindowMenuBarVisibility: (visible) ->
remote.getCurrentWindow().setMenuBarVisibility(visible)
getPrimaryDisplayWorkAreaSize: ->
screen = remote.require 'screen'
screen.getPrimaryDisplay().workAreaSize
remote.screen.getPrimaryDisplay().workAreaSize
confirm: ({message, detailedMessage, buttons}) ->
buttons ?= {}
@@ -134,8 +137,7 @@ class ApplicationDelegate
else
buttonLabels = Object.keys(buttons)
dialog = remote.require('dialog')
chosen = dialog.showMessageBox(remote.getCurrentWindow(), {
chosen = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info'
message: message
detail: detailedMessage
@@ -157,45 +159,88 @@ class ApplicationDelegate
params = _.clone(params)
params.title ?= 'Save File'
params.defaultPath ?= getWindowLoadSettings().initialPaths[0]
dialog = remote.require('dialog')
dialog.showSaveDialog remote.getCurrentWindow(), params
remote.dialog.showSaveDialog remote.getCurrentWindow(), params
playBeepSound: ->
shell.beep()
onDidOpenLocations: (callback) ->
outerCallback = (message, detail) ->
if message is 'open-locations'
callback(detail)
outerCallback = (event, message, detail) ->
callback(detail) if message is 'open-locations'
ipc.on('message', outerCallback)
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipc.removeListener('message', outerCallback)
ipcRenderer.removeListener('message', outerCallback)
onUpdateAvailable: (callback) ->
outerCallback = (message, detail) ->
if message is 'update-available'
callback(detail)
outerCallback = (event, message, detail) ->
# TODO: Yes, this is strange that `onUpdateAvailable` is listening for
# `did-begin-downloading-update`. We currently have no mechanism to know
# if there is an update, so begin of downloading is a good proxy.
callback(detail) if message is 'did-begin-downloading-update'
ipc.on('message', outerCallback)
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipc.removeListener('message', outerCallback)
ipcRenderer.removeListener('message', outerCallback)
onDidBeginDownloadingUpdate: (callback) ->
@onUpdateAvailable(callback)
onDidBeginCheckingForUpdate: (callback) ->
outerCallback = (event, message, detail) ->
callback(detail) if message is 'checking-for-update'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onDidCompleteDownloadingUpdate: (callback) ->
outerCallback = (event, message, detail) ->
# TODO: We could rename this event to `did-complete-downloading-update`
callback(detail) if message is 'update-available'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onUpdateNotAvailable: (callback) ->
outerCallback = (event, message, detail) ->
callback(detail) if message is 'update-not-available'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onApplicationMenuCommand: (callback) ->
ipc.on('command', callback)
outerCallback = (event, args...) ->
callback(args...)
ipcRenderer.on('command', outerCallback)
new Disposable ->
ipc.removeListener('command', callback)
ipcRenderer.removeListener('command', outerCallback)
onContextMenuCommand: (callback) ->
ipc.on('context-command', callback)
outerCallback = (event, args...) ->
callback(args...)
ipcRenderer.on('context-command', outerCallback)
new Disposable ->
ipc.removeListener('context-command', callback)
ipcRenderer.removeListener('context-command', outerCallback)
didCancelWindowUnload: ->
ipc.send('did-cancel-window-unload')
ipcRenderer.send('did-cancel-window-unload')
openExternal: (url) ->
shell.openExternal(url)
disablePinchToZoom: ->
webFrame.setZoomLevelLimits(1, 1)
checkForUpdate: ->
ipcRenderer.send('check-for-update')
restartAndInstallUpdate: ->
ipcRenderer.send('install-update')
getAutoUpdateManagerState: ->
ipcRenderer.sendSync('get-auto-update-manager-state')

View File

@@ -1,17 +1,17 @@
crypto = require 'crypto'
path = require 'path'
ipc = require 'ipc'
{ipcRenderer} = require 'electron'
_ = require 'underscore-plus'
{deprecate} = require 'grim'
{CompositeDisposable, Emitter} = require 'event-kit'
{CompositeDisposable, Disposable, Emitter} = require 'event-kit'
fs = require 'fs-plus'
{mapSourcePosition} = require 'source-map-support'
Model = require './model'
WindowEventHandler = require './window-event-handler'
StylesElement = require './styles-element'
StorageFolder = require './storage-folder'
{getWindowLoadSettings} = require './window-load-settings-helpers'
StateStore = require './state-store'
{getWindowLoadSettings, setWindowLoadSettings} = require './window-load-settings-helpers'
registerDefaultCommands = require './register-default-commands'
DeserializerManager = require './deserializer-manager'
@@ -40,6 +40,8 @@ Project = require './project'
TextEditor = require './text-editor'
TextBuffer = require 'text-buffer'
Gutter = require './gutter'
TextEditorRegistry = require './text-editor-registry'
AutoUpdateManager = require './auto-update-manager'
WorkspaceElement = require './workspace-element'
PanelContainerElement = require './panel-container-element'
@@ -111,6 +113,14 @@ class AtomEnvironment extends Model
# Public: A {Workspace} instance
workspace: null
# Public: A {TextEditorRegistry} instance
textEditors: null
# Private: An {AutoUpdateManager} instance
autoUpdater: null
saveStateDebounceInterval: 1000
###
Section: Construction and Destruction
###
@@ -119,14 +129,17 @@ class AtomEnvironment extends Model
constructor: (params={}) ->
{@blobStore, @applicationDelegate, @window, @document, configDirPath, @enablePersistence, onlyLoadBaseStyleSheets} = params
@state = {version: @constructor.version}
@unloaded = false
@loadTime = null
{devMode, safeMode, resourcePath} = @getLoadSettings()
{devMode, safeMode, resourcePath, clearWindowState} = @getLoadSettings()
@emitter = new Emitter
@disposables = new CompositeDisposable
@stateStore = new StateStore('AtomEnvironments', 1)
@stateStore.clear() if clearWindowState
@deserializers = new DeserializerManager(this)
@deserializeTimings = {}
@@ -179,6 +192,9 @@ class AtomEnvironment extends Model
})
@themes.workspace = @workspace
@textEditors = new TextEditorRegistry
@autoUpdater = new AutoUpdateManager({@applicationDelegate})
@config.load()
@themes.loadBaseStylesheets()
@@ -200,19 +216,29 @@ class AtomEnvironment extends Model
@registerDefaultViewProviders()
@installUncaughtErrorHandler()
@attachSaveStateListeners()
@installWindowEventHandler()
@observeAutoHideMenuBar()
checkPortableHomeWritable = ->
responseChannel = "check-portable-home-writable-response"
ipc.on responseChannel, (response) ->
ipc.removeAllListeners(responseChannel)
ipcRenderer.on responseChannel, (event, response) ->
ipcRenderer.removeAllListeners(responseChannel)
atom.notifications.addWarning("#{response.message.replace(/([\\\.+\\-_#!])/g, '\\$1')}") if not response.writable
ipc.send('check-portable-home-writable', responseChannel)
ipcRenderer.send('check-portable-home-writable', responseChannel)
checkPortableHomeWritable()
attachSaveStateListeners: ->
saveState = => @saveState({isUnloading: false}) unless @unloaded
debouncedSaveState = _.debounce(saveState, @saveStateDebounceInterval)
@document.addEventListener('mousedown', debouncedSaveState, true)
@document.addEventListener('keydown', debouncedSaveState, true)
@disposables.add new Disposable =>
@document.removeEventListener('mousedown', debouncedSaveState, true)
@document.removeEventListener('keydown', debouncedSaveState, true)
setConfigSchema: ->
@config.setSchema null, {type: 'object', properties: _.clone(require('./config-schema'))}
@@ -241,8 +267,6 @@ class AtomEnvironment extends Model
new PaneAxisElement().initialize(model, env)
@views.addViewProvider Pane, (model, env) ->
new PaneElement().initialize(model, env)
@views.addViewProvider TextEditor, (model, env) ->
new TextEditorElement().initialize(model, env)
@views.addViewProvider(Gutter, createGutterView)
registerDefaultOpeners: ->
@@ -302,9 +326,6 @@ class AtomEnvironment extends Model
@views.clear()
@registerDefaultViewProviders()
@state.packageStates = {}
delete @state.workspace
destroy: ->
return if not @project
@@ -317,6 +338,7 @@ class AtomEnvironment extends Model
@commands.clear()
@stylesElement.remove()
@config.unobserveUserConfig()
@autoUpdater.destroy()
@uninstallWindowEventHandler()
@@ -384,12 +406,27 @@ class AtomEnvironment extends Model
inSpecMode: ->
@specMode ?= @getLoadSettings().isSpec
# Returns a {Boolean} indicating whether this the first time the window's been
# loaded.
isFirstLoad: ->
@firstLoad ?= @getLoadSettings().firstLoad
# Public: Get the version of the Atom application.
#
# Returns the version text {String}.
getVersion: ->
@appVersion ?= @getLoadSettings().appVersion
# Returns the release channel as a {String}. Will return one of `'dev', 'beta', 'stable'`
getReleaseChannel: ->
version = @getVersion()
if version.indexOf('beta') > -1
'beta'
else if version.indexOf('dev') > -1
'dev'
else
'stable'
# Public: Returns a {Boolean} that is `true` if the current version is an official release.
isReleasedVersion: ->
not /\w{7}/.test(@getVersion()) # Check if the release is a 7-character SHA prefix
@@ -492,7 +529,7 @@ class AtomEnvironment extends Model
# Extended: Reload the current window.
reload: ->
@applicationDelegate.restartWindow()
@applicationDelegate.reloadWindow()
# Extended: Returns a {Boolean} that is `true` if the current window is maximized.
isMaximized: ->
@@ -519,16 +556,18 @@ class AtomEnvironment extends Model
# Restore the window to its previous dimensions and show it.
#
# Also restores the full screen and maximized state on the next tick to
# Restores the full screen and maximized state after the window has resized to
# prevent resize glitches.
displayWindow: ->
dimensions = @restoreWindowDimensions()
@show()
@focus()
setImmediate =>
@setFullScreen(true) if @workspace?.fullScreen
@maximize() if dimensions?.maximized and process.platform isnt 'darwin'
@restoreWindowDimensions().then =>
steps = [
@restoreWindowBackground(),
@show(),
@focus()
]
steps.push(@setFullScreen(true)) if @windowDimensions?.fullScreen
steps.push(@maximize()) if @windowDimensions?.maximized and process.platform isnt 'darwin'
Promise.all(steps)
# Get the dimensions of this window.
#
@@ -556,22 +595,24 @@ class AtomEnvironment extends Model
# * `width` The new width.
# * `height` The new height.
setWindowDimensions: ({x, y, width, height}) ->
steps = []
if width? and height?
@setSize(width, height)
steps.push(@setSize(width, height))
if x? and y?
@setPosition(x, y)
steps.push(@setPosition(x, y))
else
@center()
steps.push(@center())
Promise.all(steps)
# Returns true if the dimensions are useable, false if they should be ignored.
# Work around for https://github.com/atom/atom-shell/issues/473
isValidDimensions: ({x, y, width, height}={}) ->
width > 0 and height > 0 and x + width > 0 and y + height > 0
storeDefaultWindowDimensions: ->
dimensions = @getWindowDimensions()
if @isValidDimensions(dimensions)
localStorage.setItem("defaultWindowDimensions", JSON.stringify(dimensions))
storeWindowDimensions: ->
@windowDimensions = @getWindowDimensions()
if @isValidDimensions(@windowDimensions)
localStorage.setItem("defaultWindowDimensions", JSON.stringify(@windowDimensions))
getDefaultWindowDimensions: ->
{windowDimensions} = @getLoadSettings()
@@ -591,15 +632,16 @@ class AtomEnvironment extends Model
{x: 0, y: 0, width: Math.min(1024, width), height}
restoreWindowDimensions: ->
dimensions = @state.windowDimensions
unless @isValidDimensions(dimensions)
dimensions = @getDefaultWindowDimensions()
@setWindowDimensions(dimensions)
dimensions
unless @windowDimensions? and @isValidDimensions(@windowDimensions)
@windowDimensions = @getDefaultWindowDimensions()
@setWindowDimensions(@windowDimensions).then -> @windowDimensions
storeWindowDimensions: ->
dimensions = @getWindowDimensions()
@state.windowDimensions = dimensions if @isValidDimensions(dimensions)
restoreWindowBackground: ->
if backgroundColor = window.localStorage.getItem('atom:window-background-color')
@backgroundStylesheet = document.createElement('style')
@backgroundStylesheet.type = 'text/css'
@backgroundStylesheet.innerText = 'html, body { background: ' + backgroundColor + ' !important; }'
document.head.appendChild(@backgroundStylesheet)
storeWindowBackground: ->
return if @inSpecMode()
@@ -610,44 +652,57 @@ class AtomEnvironment extends Model
# Call this method when establishing a real application window.
startEditorWindow: ->
@commandInstaller.installAtomCommand false, (error) ->
console.warn error.message if error?
@commandInstaller.installApmCommand false, (error) ->
console.warn error.message if error?
@loadState().then (state) =>
@windowDimensions = state?.windowDimensions
@displayWindow().then =>
@commandInstaller.installAtomCommand false, (error) ->
console.warn error.message if error?
@commandInstaller.installApmCommand false, (error) ->
console.warn error.message if error?
@disposables.add(@applicationDelegate.onDidOpenLocations(@openLocations.bind(this)))
@disposables.add(@applicationDelegate.onApplicationMenuCommand(@dispatchApplicationMenuCommand.bind(this)))
@disposables.add(@applicationDelegate.onContextMenuCommand(@dispatchContextMenuCommand.bind(this)))
@listenForUpdates()
@disposables.add(@applicationDelegate.onDidOpenLocations(@openLocations.bind(this)))
@disposables.add(@applicationDelegate.onApplicationMenuCommand(@dispatchApplicationMenuCommand.bind(this)))
@disposables.add(@applicationDelegate.onContextMenuCommand(@dispatchContextMenuCommand.bind(this)))
@listenForUpdates()
@registerDefaultTargetForKeymaps()
@registerDefaultTargetForKeymaps()
@packages.loadPackages()
@loadStateSync()
@document.body.appendChild(@views.getView(@workspace))
@packages.loadPackages()
@watchProjectPath()
startTime = Date.now()
@deserialize(state) if state?
@deserializeTimings.atom = Date.now() - startTime
@packages.activate()
@keymaps.loadUserKeymap()
@requireUserInitScript() unless @getLoadSettings().safeMode
@document.body.appendChild(@views.getView(@workspace))
@backgroundStylesheet?.remove()
@menu.update()
@watchProjectPaths()
@openInitialEmptyEditorIfNecessary()
@packages.activate()
@keymaps.loadUserKeymap()
@requireUserInitScript() unless @getLoadSettings().safeMode
@menu.update()
@openInitialEmptyEditorIfNecessary()
serialize: (options) ->
version: @constructor.version
project: @project.serialize(options)
workspace: @workspace.serialize()
packageStates: @packages.serialize()
grammars: {grammarOverridesByPath: @grammars.grammarOverridesByPath}
fullScreen: @isFullScreen()
windowDimensions: @windowDimensions
unloadEditorWindow: ->
return if not @project
@saveState({isUnloading: true})
@storeWindowBackground()
@state.grammars = {grammarOverridesByPath: @grammars.grammarOverridesByPath}
@state.project = @project.serialize()
@state.workspace = @workspace.serialize()
@packages.deactivatePackages()
@state.packageStates = @packages.packageStates
@state.fullScreen = @isFullScreen()
@saveStateSync()
@saveBlobStoreSync()
@unloaded = true
openInitialEmptyEditorIfNecessary: ->
return unless @config.get('core.openEmptyEditorOnStart')
@@ -755,7 +810,7 @@ class AtomEnvironment extends Model
@themes.load()
# Notify the browser project of the window's current project path
watchProjectPath: ->
watchProjectPaths: ->
@disposables.add @project.onDidChangePaths =>
@applicationDelegate.setRepresentedDirectoryPaths(@project.getPaths())
@@ -780,45 +835,44 @@ class AtomEnvironment extends Model
@blobStore.save()
saveStateSync: ->
return unless @enablePersistence
saveState: (options) ->
return Promise.resolve() unless @enablePersistence
if storageKey = @getStateKey(@project?.getPaths())
@getStorageFolder().store(storageKey, @state)
new Promise (resolve, reject) =>
window.requestIdleCallback =>
return if not @project
state = @serialize(options)
savePromise =
if storageKey = @getStateKey(@project?.getPaths())
@stateStore.save(storageKey, state)
else
@applicationDelegate.setTemporaryWindowState(state)
savePromise.catch(reject).then(resolve)
loadState: ->
if @enablePersistence
if stateKey = @getStateKey(@getLoadSettings().initialPaths)
@stateStore.load(stateKey)
else
@applicationDelegate.getTemporaryWindowState()
else
@getCurrentWindow().loadSettings.windowState = JSON.stringify(@state)
Promise.resolve(null)
loadStateSync: ->
return unless @enablePersistence
startTime = Date.now()
if stateKey = @getStateKey(@getLoadSettings().initialPaths)
if state = @getStorageFolder().load(stateKey)
@state = state
if not @state? and windowState = @getLoadSettings().windowState
try
if state = JSON.parse(@getLoadSettings().windowState)
@state = state
catch error
console.warn "Error parsing window state: #{statePath} #{error.stack}", error
@deserializeTimings.atom = Date.now() - startTime
if grammarOverridesByPath = @state.grammars?.grammarOverridesByPath
deserialize: (state) ->
if grammarOverridesByPath = state.grammars?.grammarOverridesByPath
@grammars.grammarOverridesByPath = grammarOverridesByPath
@setFullScreen(@state.fullScreen)
@setFullScreen(state.fullScreen)
@packages.packageStates = @state.packageStates ? {}
@packages.packageStates = state.packageStates ? {}
startTime = Date.now()
@project.deserialize(@state.project, @deserializers) if @state.project?
@project.deserialize(state.project, @deserializers) if state.project?
@deserializeTimings.project = Date.now() - startTime
startTime = Date.now()
@workspace.deserialize(@state.workspace, @deserializers) if @state.workspace?
@workspace.deserialize(state.workspace, @deserializers) if state.workspace?
@deserializeTimings.workspace = Date.now() - startTime
getStateKey: (paths) ->
@@ -831,9 +885,6 @@ class AtomEnvironment extends Model
getConfigDirPath: ->
@configDirPath ?= process.env.ATOM_HOME
getStorageFolder: ->
@storageFolder ?= new StorageFolder(@getConfigDirPath())
getUserInitScriptPath: ->
initScriptPath = fs.resolve(@getConfigDirPath(), 'init', ['js', 'coffee'])
initScriptPath ? path.join(@getConfigDirPath(), 'init.coffee')
@@ -847,6 +898,7 @@ class AtomEnvironment extends Model
detail: error.message
dismissable: true
# TODO: We should deprecate the update events here, and use `atom.autoUpdater` instead
onUpdateAvailable: (callback) ->
@emitter.on 'update-available', callback
@@ -854,7 +906,8 @@ class AtomEnvironment extends Model
@emitter.emit 'update-available', details
listenForUpdates: ->
@disposables.add(@applicationDelegate.onUpdateAvailable(@updateAvailable.bind(this)))
# listen for updates available locally (that have been successfully downloaded)
@disposables.add(@autoUpdater.onDidCompleteDownloadingUpdate(@updateAvailable.bind(this)))
setBodyPlatformClass: ->
@document.body.classList.add("platform-#{process.platform}")
@@ -876,8 +929,8 @@ class AtomEnvironment extends Model
openLocations: (locations) ->
needsProjectPaths = @project?.getPaths().length is 0
for {pathToOpen, initialLine, initialColumn} in locations
if pathToOpen? and needsProjectPaths
for {pathToOpen, initialLine, initialColumn, forceAddToWindow} in locations
if pathToOpen? and (needsProjectPaths or forceAddToWindow)
if fs.existsSync(pathToOpen)
@project.addPath(pathToOpen)
else if fs.existsSync(path.dirname(pathToOpen))

View File

@@ -0,0 +1,73 @@
'use babel'
import {Emitter, CompositeDisposable} from 'event-kit'
export default class AutoUpdateManager {
constructor ({applicationDelegate}) {
this.applicationDelegate = applicationDelegate
this.subscriptions = new CompositeDisposable()
this.emitter = new Emitter()
this.subscriptions.add(
applicationDelegate.onDidBeginCheckingForUpdate(() => {
this.emitter.emit('did-begin-checking-for-update')
}),
applicationDelegate.onDidBeginDownloadingUpdate(() => {
this.emitter.emit('did-begin-downloading-update')
}),
applicationDelegate.onDidCompleteDownloadingUpdate((details) => {
this.emitter.emit('did-complete-downloading-update', details)
}),
applicationDelegate.onUpdateNotAvailable(() => {
this.emitter.emit('update-not-available')
})
)
}
destroy () {
this.subscriptions.dispose()
this.emitter.dispose()
}
checkForUpdate () {
this.applicationDelegate.checkForUpdate()
}
restartAndInstallUpdate () {
this.applicationDelegate.restartAndInstallUpdate()
}
getState () {
return this.applicationDelegate.getAutoUpdateManagerState()
}
platformSupportsUpdates () {
return atom.getReleaseChannel() !== 'dev' && this.getState() !== 'unsupported'
}
onDidBeginCheckingForUpdate (callback) {
return this.emitter.on('did-begin-checking-for-update', callback)
}
onDidBeginDownloadingUpdate (callback) {
return this.emitter.on('did-begin-downloading-update', callback)
}
onDidCompleteDownloadingUpdate (callback) {
return this.emitter.on('did-complete-downloading-update', callback)
}
// TODO: When https://github.com/atom/electron/issues/4587 is closed, we can
// add an update-available event.
// onUpdateAvailable (callback) {
// return this.emitter.on('update-available', callback)
// }
onUpdateNotAvailable (callback) {
return this.emitter.on('update-not-available', callback)
}
getPlatform () {
return process.platform
}
}

View File

@@ -1,6 +1,4 @@
app = require 'app'
ipc = require 'ipc'
Menu = require 'menu'
{app, Menu} = require 'electron'
_ = require 'underscore-plus'
# Used to manage the global application menu.

View File

@@ -2,14 +2,10 @@ AtomWindow = require './atom-window'
ApplicationMenu = require './application-menu'
AtomProtocolHandler = require './atom-protocol-handler'
AutoUpdateManager = require './auto-update-manager'
BrowserWindow = require 'browser-window'
StorageFolder = require '../storage-folder'
Menu = require 'menu'
app = require 'app'
dialog = require 'dialog'
shell = require 'shell'
ipcHelpers = require '../ipc-helpers'
{BrowserWindow, Menu, app, dialog, ipcMain, shell} = require 'electron'
fs = require 'fs-plus'
ipc = require 'ipc'
path = require 'path'
os = require 'os'
net = require 'net'
@@ -51,7 +47,7 @@ class AtomApplication
client = net.connect {path: options.socketPath}, ->
client.write JSON.stringify(options), ->
client.end()
app.terminate()
app.quit()
client.on 'error', createAtomApplication
@@ -65,7 +61,7 @@ class AtomApplication
exit: (status) -> app.exit(status)
constructor: (options) ->
{@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, timeout} = options
{@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, timeout, clearWindowState} = options
@socketPath = null if options.test
@@ -89,16 +85,16 @@ class AtomApplication
else
@loadState(options) or @openPath(options)
openWithOptions: ({pathsToOpen, executedFrom, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, profileStartup, timeout}) ->
openWithOptions: ({initialPaths, pathsToOpen, executedFrom, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, profileStartup, timeout, clearWindowState, addToLastWindow}) ->
if test
@runTests({headless: true, devMode, @resourcePath, executedFrom, pathsToOpen, logFile, timeout})
else if pathsToOpen.length > 0
@openPaths({pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup})
@openPaths({initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState, addToLastWindow})
else if urlsToOpen.length > 0
@openUrl({urlToOpen, devMode, safeMode}) for urlToOpen in urlsToOpen
else
# Always open a editor window if this is the first instance of Atom.
@openPath({pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup})
@openPath({initialPaths, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState, addToLastWindow})
# Public: Removes the {AtomWindow} from the global window list.
removeWindow: (window) ->
@@ -179,7 +175,6 @@ class AtomApplication
@on 'application:open-documentation', -> shell.openExternal('https://atom.io/docs/latest/?app')
@on 'application:open-discussions', -> shell.openExternal('https://discuss.atom.io')
@on 'application:open-roadmap', -> shell.openExternal('https://atom.io/roadmap?app')
@on 'application:open-faq', -> shell.openExternal('https://atom.io/faq')
@on 'application:open-terms-of-use', -> shell.openExternal('https://atom.io/terms')
@on 'application:report-issue', -> shell.openExternal('https://github.com/atom/atom/blob/master/CONTRIBUTING.md#submitting-issues')
@@ -212,7 +207,6 @@ class AtomApplication
@openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md'))
app.on 'before-quit', =>
@saveState(false)
@quitting = true
app.on 'will-quit', =>
@@ -232,7 +226,7 @@ class AtomApplication
@emit('application:new-window')
# A request from the associated render process to open a new render process.
ipc.on 'open', (event, options) =>
ipcMain.on 'open', (event, options) =>
window = @windowForEvent(event)
if options?
if typeof options.pathsToOpen is 'string'
@@ -245,44 +239,80 @@ class AtomApplication
else
@promptForPathToOpen('all', {window})
ipc.on 'update-application-menu', (event, template, keystrokesByCommand) =>
ipcMain.on 'update-application-menu', (event, template, keystrokesByCommand) =>
win = BrowserWindow.fromWebContents(event.sender)
@applicationMenu.update(win, template, keystrokesByCommand)
ipc.on 'run-package-specs', (event, packageSpecPath) =>
ipcMain.on 'run-package-specs', (event, packageSpecPath) =>
@runTests({resourcePath: @devResourcePath, pathsToOpen: [packageSpecPath], headless: false})
ipc.on 'command', (event, command) =>
ipcMain.on 'command', (event, command) =>
@emit(command)
ipc.on 'window-command', (event, command, args...) ->
ipcMain.on 'window-command', (event, command, args...) ->
win = BrowserWindow.fromWebContents(event.sender)
win.emit(command, args...)
ipc.on 'call-window-method', (event, method, args...) ->
ipcMain.on 'call-window-method', (event, method, args...) ->
win = BrowserWindow.fromWebContents(event.sender)
win[method](args...)
ipc.on 'pick-folder', (event, responseChannel) =>
ipcMain.on 'pick-folder', (event, responseChannel) =>
@promptForPath "folder", (selectedPaths) ->
event.sender.send(responseChannel, selectedPaths)
ipc.on 'did-cancel-window-unload', =>
ipcHelpers.respondTo 'set-window-size', (win, width, height) ->
win.setSize(width, height)
ipcHelpers.respondTo 'set-window-position', (win, x, y) ->
win.setPosition(x, y)
ipcHelpers.respondTo 'center-window', (win) ->
win.center()
ipcHelpers.respondTo 'focus-window', (win) ->
win.focus()
ipcHelpers.respondTo 'show-window', (win) ->
win.show()
ipcHelpers.respondTo 'hide-window', (win) ->
win.hide()
ipcHelpers.respondTo 'get-temporary-window-state', (win) ->
win.temporaryState
ipcHelpers.respondTo 'set-temporary-window-state', (win, state) ->
win.temporaryState = state
ipcMain.on 'did-cancel-window-unload', =>
@quitting = false
clipboard = require '../safe-clipboard'
ipc.on 'write-text-to-selection-clipboard', (event, selectedText) ->
ipcMain.on 'write-text-to-selection-clipboard', (event, selectedText) ->
clipboard.writeText(selectedText, 'selection')
ipc.on 'write-to-stdout', (event, output) ->
ipcMain.on 'write-to-stdout', (event, output) ->
process.stdout.write(output)
ipc.on 'write-to-stderr', (event, output) ->
ipcMain.on 'write-to-stderr', (event, output) ->
process.stderr.write(output)
ipc.on 'add-recent-document', (event, filename) ->
ipcMain.on 'add-recent-document', (event, filename) ->
app.addRecentDocument(filename)
ipcMain.on 'execute-javascript-in-dev-tools', (event, code) ->
event.sender.devToolsWebContents?.executeJavaScript(code)
ipcMain.on 'check-for-update', =>
@autoUpdateManager.check()
ipcMain.on 'get-auto-update-manager-state', (event) =>
event.returnValue = @autoUpdateManager.getState()
ipcMain.on 'execute-javascript-in-dev-tools', (event, code) ->
event.sender.devToolsWebContents?.executeJavaScript(code)
setupDockMenu: ->
if process.platform is 'darwin'
dockMenu = Menu.buildFromTemplate [
@@ -350,7 +380,7 @@ class AtomApplication
_.find @windows, (atomWindow) ->
atomWindow.devMode is devMode and atomWindow.containsPaths(pathsToOpen)
# Returns the {AtomWindow} for the given ipc event.
# Returns the {AtomWindow} for the given ipcMain event.
windowForEvent: ({sender}) ->
window = BrowserWindow.fromWebContents(sender)
_.find @windows, ({browserWindow}) -> window is browserWindow
@@ -387,8 +417,9 @@ class AtomApplication
# :safeMode - Boolean to control the opened window's safe mode.
# :profileStartup - Boolean to control creating a profile of the startup time.
# :window - {AtomWindow} to open file paths in.
openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window} = {}) ->
@openPaths({pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window})
# :addToLastWindow - Boolean of whether this should be opened in last focused window.
openPath: ({initialPaths, pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow} = {}) ->
@openPaths({initialPaths, pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow})
# Public: Opens multiple paths, in existing windows if possible.
#
@@ -400,10 +431,12 @@ class AtomApplication
# :safeMode - Boolean to control the opened window's safe mode.
# :windowDimensions - Object with height and width keys.
# :window - {AtomWindow} to open file paths in.
openPaths: ({pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window}={}) ->
# :addToLastWindow - Boolean of whether this should be opened in last focused window.
openPaths: ({initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window, clearWindowState, addToLastWindow}={}) ->
devMode = Boolean(devMode)
safeMode = Boolean(safeMode)
locationsToOpen = (@locationForPathToOpen(pathToOpen, executedFrom) for pathToOpen in pathsToOpen)
clearWindowState = Boolean(clearWindowState)
locationsToOpen = (@locationForPathToOpen(pathToOpen, executedFrom, addToLastWindow) for pathToOpen in pathsToOpen)
pathsToOpen = (locationToOpen.pathToOpen for locationToOpen in locationsToOpen)
unless pidToKillWhenClosed or newWindow
@@ -412,6 +445,7 @@ class AtomApplication
unless existingWindow?
if currentWindow = window ? @lastFocusedWindow
existingWindow = currentWindow if (
addToLastWindow or
currentWindow.devMode is devMode and
(
stats.every((stat) -> stat.isFile?()) or
@@ -435,7 +469,7 @@ class AtomApplication
windowInitializationScript ?= require.resolve('../initialize-application-window')
resourcePath ?= @resourcePath
windowDimensions ?= @getDimensionsForNewWindow()
openedWindow = new AtomWindow({locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup})
openedWindow = new AtomWindow({initialPaths, locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState})
if pidToKillWhenClosed?
@pidsToOpenWindows[pidToKillWhenClosed] = openedWindow
@@ -472,13 +506,14 @@ class AtomApplication
if loadSettings = window.getLoadSettings()
states.push(initialPaths: loadSettings.initialPaths)
if states.length > 0 or allowEmpty
@storageFolder.store('application.json', states)
@storageFolder.storeSync('application.json', states)
loadState: (options) ->
if (states = @storageFolder.load('application.json'))?.length > 0
for state in states
@openWithOptions(_.extend(options, {
pathsToOpen: state.initialPaths
initialPaths: state.initialPaths
pathsToOpen: state.initialPaths.filter (directoryPath) -> fs.isDirectorySync(directoryPath)
urlsToOpen: []
devMode: @devMode
safeMode: @safeMode
@@ -511,7 +546,7 @@ class AtomApplication
if pack.urlMain
packagePath = @packages.resolvePackagePath(packageName)
windowInitializationScript = path.resolve(packagePath, pack.urlMain)
windowDimensions = @focusedWindow()?.getDimensions()
windowDimensions = @getDimensionsForNewWindow()
new AtomWindow({windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions})
else
console.log "Package '#{pack.name}' does not have a url main: #{urlToOpen}"
@@ -580,7 +615,7 @@ class AtomApplication
catch error
require.resolve(path.resolve(__dirname, '..', '..', 'spec', 'jasmine-test-runner'))
locationForPathToOpen: (pathToOpen, executedFrom='') ->
locationForPathToOpen: (pathToOpen, executedFrom='', forceAddToWindow) ->
return {pathToOpen} unless pathToOpen
pathToOpen = pathToOpen.replace(/[:\s]+$/, '')
@@ -596,7 +631,7 @@ class AtomApplication
unless url.parse(pathToOpen).protocol?
pathToOpen = path.resolve(executedFrom, fs.normalize(pathToOpen))
{pathToOpen, initialLine, initialColumn}
{pathToOpen, initialLine, initialColumn, forceAddToWindow}
# Opens a native dialog to prompt the user for a path.
#

View File

@@ -1,6 +1,6 @@
fs = require 'fs-plus'
path = require 'path'
ipc = require 'ipc'
{ipcMain} = require 'electron'
module.exports =
class AtomPortable
@@ -30,6 +30,6 @@ class AtomPortable
catch error
message = "Failed to use portable Atom home directory (#{@getPortableAtomHomePath()}). Using the default instead (#{defaultHome}). #{error.message}"
ipc.on 'check-portable-home-writable', (event) ->
ipcMain.on 'check-portable-home-writable', (event) ->
event.sender.send 'check-portable-home-writable-response', {writable, message}
writable

View File

@@ -1,7 +1,6 @@
app = require 'app'
{app, protocol} = require 'electron'
fs = require 'fs'
path = require 'path'
protocol = require 'protocol'
# Handles requests with 'atom' protocol.
#

View File

@@ -1,6 +1,4 @@
BrowserWindow = require 'browser-window'
app = require 'app'
dialog = require 'dialog'
{BrowserWindow, app, dialog} = require 'electron'
path = require 'path'
fs = require 'fs'
url = require 'url'
@@ -19,7 +17,7 @@ class AtomWindow
isSpec: null
constructor: (settings={}) ->
{@resourcePath, pathToOpen, locationsToOpen, @isSpec, @headless, @safeMode, @devMode} = settings
{@resourcePath, initialPaths, pathToOpen, locationsToOpen, @isSpec, @headless, @safeMode, @devMode} = settings
locationsToOpen ?= [{pathToOpen}] if pathToOpen
locationsToOpen ?= []
@@ -43,19 +41,13 @@ class AtomWindow
@handleEvents()
loadSettings = _.extend({}, settings)
loadSettings.windowState ?= '{}'
loadSettings.appVersion = app.getVersion()
loadSettings.resourcePath = @resourcePath
loadSettings.devMode ?= false
loadSettings.safeMode ?= false
loadSettings.atomHome = process.env.ATOM_HOME
# Only send to the first non-spec window created
if @constructor.includeShellLoadTime and not @isSpec
@constructor.includeShellLoadTime = false
loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime
loadSettings.initialPaths =
loadSettings.clearWindowState ?= false
loadSettings.initialPaths ?=
for {pathToOpen} in locationsToOpen when pathToOpen
if fs.statSyncNoException(pathToOpen).isFile?()
path.dirname(pathToOpen)
@@ -64,24 +56,26 @@ class AtomWindow
loadSettings.initialPaths.sort()
# Only send to the first non-spec window created
if @constructor.includeShellLoadTime and not @isSpec
@constructor.includeShellLoadTime = false
loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime
@browserWindow.loadSettings = loadSettings
@browserWindow.once 'window:loaded', =>
@emit 'window:loaded'
@loaded = true
@setLoadSettings(loadSettings)
@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: (loadSettingsObj) ->
# Ignore the windowState when passing loadSettings via URL, since it could
# be quite large.
loadSettings = _.clone(loadSettingsObj)
delete loadSettings['windowState']
@browserWindow.loadUrl url.format
setLoadSettings: (loadSettings) ->
@browserWindow.loadURL url.format
protocol: 'file'
pathname: "#{@resourcePath}/static/index.html"
slashes: true
@@ -89,7 +83,7 @@ class AtomWindow
getLoadSettings: ->
if @browserWindow.webContents? and not @browserWindow.webContents.isLoading()
hash = url.parse(@browserWindow.webContents.getUrl()).hash.substr(1)
hash = url.parse(@browserWindow.webContents.getURL()).hash.substr(1)
JSON.parse(decodeURIComponent(hash))
hasProjectPath: -> @getLoadSettings().initialPaths?.length > 0
@@ -121,6 +115,9 @@ class AtomWindow
false
handleEvents: ->
@browserWindow.on 'close', ->
global.atomApplication.saveState(false)
@browserWindow.on 'closed', =>
global.atomApplication.removeWindow(this)
@@ -144,10 +141,10 @@ class AtomWindow
detail: 'Please report this issue to https://github.com/atom/atom'
switch chosen
when 0 then @browserWindow.destroy()
when 1 then @browserWindow.restart()
when 1 then @browserWindow.reload()
@browserWindow.webContents.on 'will-navigate', (event, url) =>
unless url is @browserWindow.webContents.getUrl()
unless url is @browserWindow.webContents.getURL()
event.preventDefault()
@setupContextMenu()
@@ -220,6 +217,6 @@ class AtomWindow
isSpecWindow: -> @isSpec
reload: -> @browserWindow.restart()
reload: -> @browserWindow.reload()
toggleDevTools: -> @browserWindow.toggleDevTools()

View File

@@ -29,26 +29,34 @@ class AutoUpdateManager
if process.platform is 'win32'
autoUpdater = require './auto-updater-win32'
else
autoUpdater = require 'auto-updater'
{autoUpdater} = require 'electron'
autoUpdater.on 'error', (event, message) =>
@setState(ErrorState)
console.error "Error Downloading Update: #{message}"
autoUpdater.setFeedUrl @feedUrl
autoUpdater.setFeedURL @feedUrl
autoUpdater.on 'checking-for-update', =>
@setState(CheckingState)
@emitWindowEvent('checking-for-update')
autoUpdater.on 'update-not-available', =>
@setState(NoUpdateAvailableState)
@emitWindowEvent('update-not-available')
autoUpdater.on 'update-available', =>
@setState(DownladingState)
# We use sendMessage to send an event called 'update-available' in 'update-downloaded'
# once the update download is complete. This mismatch between the electron
# autoUpdater events is unfortunate but in the interest of not changing the
# one existing event handled by applicationDelegate
@emitWindowEvent('did-begin-downloading-update')
@emit('did-begin-download')
autoUpdater.on 'update-downloaded', (event, releaseNotes, @releaseVersion) =>
@setState(UpdateAvailableState)
@emitUpdateAvailableEvent(@getWindows()...)
@emitUpdateAvailableEvent()
@config.onDidChange 'core.automaticallyUpdate', ({newValue}) =>
if newValue
@@ -64,10 +72,14 @@ class AutoUpdateManager
when 'linux'
@setState(UnsupportedState)
emitUpdateAvailableEvent: (windows...) ->
emitUpdateAvailableEvent: ->
return unless @releaseVersion?
for atomWindow in windows
atomWindow.sendMessage('update-available', {@releaseVersion})
@emitWindowEvent('update-available', {@releaseVersion})
return
emitWindowEvent: (eventName, payload) ->
for atomWindow in @getWindows()
atomWindow.sendMessage(eventName, payload)
return
setState: (state) ->
@@ -104,7 +116,7 @@ class AutoUpdateManager
onUpdateNotAvailable: =>
autoUpdater.removeListener 'error', @onUpdateError
dialog = require 'dialog'
{dialog} = require 'electron'
dialog.showMessageBox
type: 'info'
buttons: ['OK']
@@ -115,7 +127,7 @@ class AutoUpdateManager
onUpdateError: (event, message) =>
autoUpdater.removeListener 'update-not-available', @onUpdateNotAvailable
dialog = require 'dialog'
{dialog} = require 'electron'
dialog.showMessageBox
type: 'warning'
buttons: ['OK']

View File

@@ -5,13 +5,13 @@ SquirrelUpdate = require './squirrel-update'
class AutoUpdater
_.extend @prototype, EventEmitter.prototype
setFeedUrl: (@updateUrl) ->
setFeedURL: (@updateUrl) ->
quitAndInstall: ->
if SquirrelUpdate.existsSync()
SquirrelUpdate.restartAtom(require('app'))
SquirrelUpdate.restartAtom(require('electron').app)
else
require('auto-updater').quitAndInstall()
require('electron').autoUpdater.quitAndInstall()
downloadUpdate: (callback) ->
SquirrelUpdate.spawn ['--download', @updateUrl], (error, stdout) ->

View File

@@ -1,4 +1,4 @@
Menu = require 'menu'
{Menu} = require 'electron'
module.exports =
class ContextMenu

View File

@@ -4,10 +4,10 @@ process.on 'uncaughtException', (error={}) ->
console.log(error.message) if error.message?
console.log(error.stack) if error.stack?
crashReporter = require 'crash-reporter'
app = require 'app'
{crashReporter, app} = require 'electron'
fs = require 'fs-plus'
path = require 'path'
temp = require 'temp'
yargs = require 'yargs'
console.log = require 'nslog'
@@ -32,6 +32,11 @@ start = ->
app.on 'open-url', addUrlToOpen
app.on 'will-finish-launching', setupCrashReporter
if args.userDataDir?
app.setPath('userData', args.userDataDir)
else if args.test
app.setPath('userData', temp.mkdirSync('atom-test-data'))
app.on 'ready', ->
app.removeListener 'open-file', addPathToOpen
app.removeListener 'open-url', addUrlToOpen
@@ -54,12 +59,12 @@ handleStartupEventWithSquirrel = ->
SquirrelUpdate.handleStartupEvent(app, squirrelCommand)
setupCrashReporter = ->
crashReporter.start(productName: 'Atom', companyName: 'GitHub')
crashReporter.start(productName: 'Atom', companyName: 'GitHub', submitURL: 'http://54.249.141.255:1127/post')
setupAtomHome = ({setPortable}) ->
return if process.env.ATOM_HOME
atomHome = path.join(app.getHomeDir(), '.atom')
atomHome = path.join(app.getPath('home'), '.atom')
AtomPortable = require './atom-portable'
if setPortable and not AtomPortable.isPortableInstall(process.platform, process.env.ATOM_HOME, atomHome)
@@ -81,6 +86,15 @@ setupCompileCache = ->
compileCache = require('../compile-cache')
compileCache.setAtomHomeDirectory(process.env.ATOM_HOME)
writeFullVersion = ->
process.stdout.write """
Atom : #{app.getVersion()}
Electron: #{process.versions.electron}
Chrome : #{process.versions.chrome}
Node : #{process.versions.node}
"""
parseCommandLine = ->
version = app.getVersion()
options = yargs(process.argv[1..]).wrap(100)
@@ -116,9 +130,12 @@ parseCommandLine = ->
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.alias('t', 'test').boolean('t').describe('t', 'Run the specified specs and exit with error code on failures.')
options.string('timeout').describe('timeout', 'When in test mode, waits until the specified time (in minutes) and kills the process (exit code: 130).')
options.alias('v', 'version').boolean('v').describe('v', 'Print the version.')
options.alias('v', 'version').boolean('v').describe('v', 'Print the version information.')
options.alias('w', 'wait').boolean('w').describe('w', 'Wait for window to be closed before returning.')
options.alias('a', 'add').boolean('a').describe('add', 'Open path as a new project in last used window.')
options.string('socket-path')
options.string('user-data-dir')
options.boolean('clear-window-state').describe('clear-window-state', 'Delete all Atom environment state.')
args = options.argv
@@ -127,9 +144,10 @@ parseCommandLine = ->
process.exit(0)
if args.version
process.stdout.write("#{version}\n")
writeFullVersion()
process.exit(0)
addToLastWindow = args['add']
executedFrom = args['executed-from']?.toString() ? process.cwd()
devMode = args['dev']
safeMode = args['safe']
@@ -140,9 +158,11 @@ parseCommandLine = ->
pidToKillWhenClosed = args['pid'] if args['wait']
logFile = args['log-file']
socketPath = args['socket-path']
userDataDir = args['user-data-dir']
profileStartup = args['profile-startup']
clearWindowState = args['clear-window-state']
urlsToOpen = []
devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH ? path.join(app.getHomeDir(), 'github', 'atom')
devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH ? path.join(app.getPath('home'), 'github', 'atom')
setPortable = args.portable
if args['resource-path']
@@ -164,6 +184,7 @@ parseCommandLine = ->
{resourcePath, devResourcePath, pathsToOpen, urlsToOpen, executedFrom, test,
version, pidToKillWhenClosed, devMode, safeMode, newWindow,
logFile, socketPath, profileStartup, timeout, setPortable}
logFile, socketPath, userDataDir, profileStartup, timeout, setPortable,
clearWindowState, addToLastWindow}
start()

View File

@@ -11,15 +11,18 @@ exeName = path.basename(process.execPath)
if process.env.SystemRoot
system32Path = path.join(process.env.SystemRoot, 'System32')
regPath = path.join(system32Path, 'reg.exe')
powershellPath = path.join(system32Path, 'WindowsPowerShell', 'v1.0', 'powershell.exe')
setxPath = path.join(system32Path, 'setx.exe')
else
regPath = 'reg.exe'
powershellPath = 'powershell.exe'
setxPath = 'setx.exe'
# Registry keys used for context menu
fileKeyPath = 'HKCU\\Software\\Classes\\*\\shell\\Atom'
directoryKeyPath = 'HKCU\\Software\\Classes\\directory\\shell\\Atom'
backgroundKeyPath = 'HKCU\\Software\\Classes\\directory\\background\\shell\\Atom'
applicationsKeyPath = 'HKCU\\Software\\Classes\\Applications\\atom.exe'
environmentKeyPath = 'HKCU\\Environment'
# Spawn a command and invoke the callback when it completes with an error
@@ -43,11 +46,31 @@ spawn = (command, args, callback) ->
error?.code ?= code
error?.stdout ?= stdout
callback?(error, stdout)
# This is necessary if using Powershell 2 on Windows 7 to get the events to raise
# http://stackoverflow.com/questions/9155289/calling-powershell-from-nodejs
spawnedProcess.stdin.end()
# Spawn reg.exe and callback when it completes
spawnReg = (args, callback) ->
spawn(regPath, args, callback)
# Spawn powershell.exe and callback when it completes
spawnPowershell = (args, callback) ->
# set encoding and execute the command, capture the output, and return it via .NET's console in order to have consistent UTF-8 encoding
# http://stackoverflow.com/questions/22349139/utf-8-output-from-powershell
# to address https://github.com/atom/atom/issues/5063
args[0] = """
[Console]::OutputEncoding=[System.Text.Encoding]::UTF8
$output=#{args[0]}
[Console]::WriteLine($output)
"""
args.unshift('-command')
args.unshift('RemoteSigned')
args.unshift('-ExecutionPolicy')
args.unshift('-noprofile')
spawn(powershellPath, args, callback)
# Spawn setx.exe and callback when it completes
spawnSetx = (args, callback) ->
spawn(setxPath, args, callback)
@@ -64,6 +87,10 @@ installContextMenu = (callback) ->
args.push('/f')
spawnReg(args, callback)
installFileHandler = (callback) ->
args = ["#{applicationsKeyPath}\\shell\\open\\command", '/ve', '/d', "\"#{process.execPath}\" \"%1\""]
addToRegistry(args, callback)
installMenu = (keyPath, arg, callback) ->
args = [keyPath, '/ve', '/d', 'Open with Atom']
addToRegistry args, ->
@@ -74,48 +101,17 @@ installContextMenu = (callback) ->
installMenu fileKeyPath, '%1', ->
installMenu directoryKeyPath, '%1', ->
installMenu(backgroundKeyPath, '%V', callback)
isAscii = (text) ->
index = 0
while index < text.length
return false if text.charCodeAt(index) > 127
index++
true
installMenu backgroundKeyPath, '%V', ->
installFileHandler(callback)
# Get the user's PATH environment variable registry value.
getPath = (callback) ->
spawnReg ['query', environmentKeyPath, '/v', 'Path'], (error, stdout) ->
spawnPowershell ['[environment]::GetEnvironmentVariable(\'Path\',\'User\')'], (error, stdout) ->
if error?
if error.code is 1
# FIXME Don't overwrite path when reading value is disabled
# https://github.com/atom/atom/issues/5092
if stdout.indexOf('ERROR: Registry editing has been disabled by your administrator.') isnt -1
return callback(error)
return callback(error)
# The query failed so the Path does not exist yet in the registry
return callback(null, '')
else
return callback(error)
# Registry query output is in the form:
#
# HKEY_CURRENT_USER\Environment
# Path REG_SZ C:\a\folder\on\the\path;C\another\folder
#
lines = stdout.split(/[\r\n]+/).filter (line) -> line
segments = lines[lines.length - 1]?.split(' ')
if segments[1] is 'Path' and segments.length >= 3
pathEnv = segments?[3..].join(' ')
if isAscii(pathEnv)
callback(null, pathEnv)
else
# FIXME Don't corrupt non-ASCII PATH values
# https://github.com/atom/atom/issues/5063
callback(new Error('PATH contains non-ASCII values'))
else
callback(new Error('Registry query for PATH failed'))
pathOutput = stdout.replace(/^\s+|\s+$/g, '')
callback(null, pathOutput)
# Uninstall the Open with Atom explorer context menu items via the registry.
uninstallContextMenu = (callback) ->
@@ -124,7 +120,8 @@ uninstallContextMenu = (callback) ->
deleteFromRegistry fileKeyPath, ->
deleteFromRegistry directoryKeyPath, ->
deleteFromRegistry(backgroundKeyPath, callback)
deleteFromRegistry backgroundKeyPath, ->
deleteFromRegistry(applicationsKeyPath, callback)
# Add atom and apm to the PATH
#

View File

@@ -46,7 +46,7 @@ class BufferedNodeProcess extends BufferedProcess
options ?= {}
options.env ?= Object.create(process.env)
options.env['ATOM_SHELL_INTERNAL_RUN_AS_NODE'] = 1
options.env['ELECTRON_RUN_AS_NODE'] = 1
args = args?.slice() ? []
args.unshift(command)

View File

@@ -85,5 +85,5 @@ parseAlpha = (alpha) ->
numberToHexString = (number) ->
hex = number.toString(16)
hex = "0#{hex}" if number < 10
hex = "0#{hex}" if number < 16
hex

View File

@@ -108,6 +108,10 @@ module.exports =
description: 'Automatically update Atom when a new release is available.'
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'
default: true
editor:
type: 'object'
@@ -171,7 +175,7 @@ module.exports =
tabLength:
type: 'integer'
default: 2
enum: [1, 2, 3, 4, 6, 8]
minimum: 1
description: 'Number of spaces used to represent a tab.'
softWrap:
type: 'boolean'

View File

@@ -319,6 +319,23 @@ ScopeDescriptor = require './scope-descriptor'
# * line breaks - `line breaks<br/>`
# * ~~strikethrough~~ - `~~strikethrough~~`
#
# #### order
#
# The settings view orders your settings alphabetically. You can override this
# ordering with the order key.
#
# ```coffee
# config:
# zSetting:
# type: 'integer'
# default: 4
# order: 1
# aSetting:
# type: 'integer'
# default: 4
# order: 2
# ```
#
# ## Best practices
#
# * Don't depend on (or write to) configuration keys outside of your keypath.

View File

@@ -4,7 +4,7 @@ CSON = require 'season'
fs = require 'fs-plus'
{calculateSpecificity, validateSelector} = require 'clear-cut'
{Disposable} = require 'event-kit'
remote = require 'remote'
{remote} = require 'electron'
MenuHelpers = require './menu-helpers'
platformContextMenu = require('../package.json')?._atomMenu?['context-menu']

View File

@@ -393,7 +393,10 @@ class DisplayBuffer extends Model
@displayLayer.foldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity))).length > 0
isFoldedAtScreenRow: (screenRow) ->
@isFoldedAtBufferRow(@bufferRowForScreenRow(screenRow))?
@isFoldedAtBufferRow(@bufferRowForScreenRow(screenRow))
isFoldableAtBufferRow: (row) ->
@tokenizedBuffer.isFoldableAtRow(row)
# Removes any folds found that contain the given buffer row.
#

View File

@@ -202,32 +202,35 @@ export default class GitRepositoryAsync {
workingDirectory = workingDirectory.replace(/\/$/, '')
// Depending on where the paths come from, they may have a '/private/'
// prefix. Standardize by stripping that out.
_path = _path.replace(/^\/private\//i, '/')
workingDirectory = workingDirectory.replace(/^\/private\//i, '/')
const originalPath = _path
const originalWorkingDirectory = workingDirectory
if (this.isCaseInsensitive) {
_path = _path.toLowerCase()
workingDirectory = workingDirectory.toLowerCase()
}
// Depending on where the paths come from, they may have a '/private/'
// prefix. Standardize by stripping that out.
_path = _path.replace(/^\/private\//, '/')
workingDirectory = workingDirectory.replace(/^\/private\//, '/')
const originalPath = _path
if (_path.indexOf(workingDirectory) === 0) {
return originalPath.substring(workingDirectory.length + 1)
return originalPath.substring(originalWorkingDirectory.length + 1)
} else if (_path === workingDirectory) {
return ''
}
if (openedWorkingDirectory) {
openedWorkingDirectory = openedWorkingDirectory.replace(/\/$/, '')
openedWorkingDirectory = openedWorkingDirectory.replace(/^\/private\//i, '/')
const originalOpenedWorkingDirectory = openedWorkingDirectory
if (this.isCaseInsensitive) {
openedWorkingDirectory = openedWorkingDirectory.toLowerCase()
}
openedWorkingDirectory = openedWorkingDirectory.replace(/\/$/, '')
openedWorkingDirectory = openedWorkingDirectory.replace(/^\/private\//, '/')
if (_path.indexOf(openedWorkingDirectory) === 0) {
return originalPath.substring(openedWorkingDirectory.length + 1)
return originalPath.substring(originalOpenedWorkingDirectory.length + 1)
} else if (_path === openedWorkingDirectory) {
return ''
}
@@ -456,7 +459,10 @@ export default class GitRepositoryAsync {
// information.
getDirectoryStatus (directoryPath) {
return this.relativizeToWorkingDirectory(directoryPath)
.then(relativePath => this._getStatus([relativePath]))
.then(relativePath => {
const pathspec = relativePath + '/**'
return this._getStatus([pathspec])
})
.then(statuses => {
return Promise.all(statuses.map(s => s.statusBit())).then(bits => {
return bits
@@ -590,7 +596,15 @@ export default class GitRepositoryAsync {
.then(([repo, headCommit]) => Promise.all([repo, headCommit.getTree()]))
.then(([repo, tree]) => {
const options = new Git.DiffOptions()
options.contextLines = 0
options.flags = Git.Diff.OPTION.DISABLE_PATHSPEC_MATCH
options.pathspec = this.relativize(_path, repo.workdir())
if (process.platform === 'win32') {
// Ignore eol of line differences on windows so that files checked in
// as LF don't report every line modified when the text contains CRLF
// endings.
options.flags |= Git.Diff.OPTION.IGNORE_WHITESPACE_EOL
}
return Git.Diff.treeToWorkdir(repo, tree, options)
})
.then(diff => this._getDiffLines(diff))
@@ -769,12 +783,17 @@ export default class GitRepositoryAsync {
// Get the current branch and update this.branch.
//
// Returns a {Promise} which resolves to the {String} branch name.
// Returns a {Promise} which resolves to a {boolean} indicating whether the
// branch name changed.
_refreshBranch () {
return this.getRepo()
.then(repo => repo.getCurrentBranch())
.then(ref => ref.name())
.then(branchName => this.branch = branchName)
.then(branchName => {
const changed = branchName !== this.branch
this.branch = branchName
return changed
})
}
// Refresh the cached ahead/behind count with the given branch.
@@ -782,10 +801,15 @@ export default class GitRepositoryAsync {
// * `branchName` The {String} name of the branch whose ahead/behind should be
// used for the refresh.
//
// Returns a {Promise} which will resolve to {null}.
// Returns a {Promise} which will resolve to a {boolean} indicating whether
// the ahead/behind count changed.
_refreshAheadBehindCount (branchName) {
return this.getAheadBehindCount(branchName)
.then(counts => this.upstream = counts)
.then(counts => {
const changed = !_.isEqual(counts, this.upstream)
this.upstream = counts
return changed
})
}
// Get the status for this repository.
@@ -800,7 +824,7 @@ export default class GitRepositoryAsync {
}
return Promise.all(projectPathsPromises)
.then(paths => paths.filter(p => p.length > 0))
.then(paths => paths.map(p => p.length > 0 ? p + '/**' : '*'))
.then(projectPaths => {
return this._getStatus(projectPaths.length > 0 ? projectPaths : null)
})
@@ -891,15 +915,15 @@ export default class GitRepositoryAsync {
// Refresh the cached status.
//
// Returns a {Promise} which will resolve to {null}.
// Returns a {Promise} which will resolve to a {boolean} indicating whether
// any statuses changed.
_refreshStatus () {
return Promise.all([this._getRepositoryStatus(), this._getSubmoduleStatuses()])
.then(([repositoryStatus, submoduleStatus]) => {
const statusesByPath = _.extend({}, repositoryStatus, submoduleStatus)
if (!_.isEqual(this.pathStatusCache, statusesByPath) && this.emitter != null) {
this.emitter.emit('did-change-statuses')
}
const changed = !_.isEqual(this.pathStatusCache, statusesByPath)
this.pathStatusCache = statusesByPath
return changed
})
}
@@ -909,11 +933,17 @@ export default class GitRepositoryAsync {
refreshStatus () {
const status = this._refreshStatus()
const branch = this._refreshBranch()
const aheadBehind = branch.then(branchName => this._refreshAheadBehindCount(branchName))
const aheadBehind = branch.then(() => this._refreshAheadBehindCount(this.branch))
this._refreshingPromise = this._refreshingPromise.then(_ => {
return Promise.all([status, branch, aheadBehind])
.then(_ => null)
.then(([statusChanged, branchChanged, aheadBehindChanged]) => {
if (this.emitter && (statusChanged || branchChanged || aheadBehindChanged)) {
this.emitter.emit('did-change-statuses')
}
return null
})
// Because all these refresh steps happen asynchronously, it's entirely
// possible the repository was destroyed while we were working. In which
// case we should just swallow the error.
@@ -1032,7 +1062,7 @@ export default class GitRepositoryAsync {
return this.getRepo()
.then(repo => {
const opts = {
flags: Git.Status.OPT.INCLUDE_UNTRACKED | Git.Status.OPT.RECURSE_UNTRACKED_DIRS | Git.Status.OPT.DISABLE_PATHSPEC_MATCH
flags: Git.Status.OPT.INCLUDE_UNTRACKED | Git.Status.OPT.RECURSE_UNTRACKED_DIRS
}
if (paths) {

View File

@@ -484,7 +484,7 @@ class GitRepository
relativeProjectPaths = @project?.getPaths()
.map (path) => @relativize(path)
.filter (path) -> path.length > 0
.map (path) -> if path.length > 0 then path + '/**' else '*'
@statusTask?.terminate()
@statusTask = Task.once @handlerPath, @getPath(), relativeProjectPaths, ({statuses, upstream, branch, submodules}) =>

View File

@@ -23,11 +23,10 @@ module.exports = ({blobStore}) ->
enablePersistence: true
})
atom.displayWindow()
atom.startEditorWindow()
atom.startEditorWindow().then ->
# Workaround for focus getting cleared upon window creation
windowFocused = ->
window.removeEventListener('focus', windowFocused)
setTimeout (-> document.querySelector('atom-workspace').focus()), 0
window.addEventListener('focus', windowFocused)
# Workaround for focus getting cleared upon window creation
windowFocused = ->
window.removeEventListener('focus', windowFocused)
setTimeout (-> document.querySelector('atom-workspace').focus()), 0
window.addEventListener('focus', windowFocused)

View File

@@ -4,17 +4,17 @@ cloneObject = (object) ->
clone
module.exports = ({blobStore}) ->
{crashReporter, remote} = require 'electron'
# Start the crash reporter before anything else.
require('crash-reporter').start(productName: 'Atom', companyName: 'GitHub')
remote = require 'remote'
crashReporter.start(productName: 'Atom', companyName: 'GitHub', submitURL: 'http://54.249.141.255:1127/post')
exitWithStatusCode = (status) ->
remote.require('app').emit('will-quit')
remote.app.emit('will-quit')
remote.process.exit(status)
try
path = require 'path'
ipc = require 'ipc'
{ipcRenderer} = require 'electron'
{getWindowLoadSettings} = require './window-load-settings-helpers'
AtomEnvironment = require '../src/atom-environment'
ApplicationDelegate = require '../src/application-delegate'
@@ -29,15 +29,15 @@ module.exports = ({blobStore}) ->
handleKeydown = (event) ->
# Reload: cmd-r / ctrl-r
if (event.metaKey or event.ctrlKey) and event.keyCode is 82
ipc.send('call-window-method', 'restart')
ipcRenderer.send('call-window-method', 'reload')
# Toggle Dev Tools: cmd-alt-i / ctrl-alt-i
if (event.metaKey or event.ctrlKey) and event.altKey and event.keyCode is 73
ipc.send('call-window-method', 'toggleDevTools')
ipcRenderer.send('call-window-method', 'toggleDevTools')
# Reload: cmd-w / ctrl-w
if (event.metaKey or event.ctrlKey) and event.keyCode is 87
ipc.send('call-window-method', 'close')
ipcRenderer.send('call-window-method', 'close')
window.addEventListener('keydown', handleKeydown, true)
@@ -68,7 +68,8 @@ module.exports = ({blobStore}) ->
logFile, headless, testPaths, buildAtomEnvironment, buildDefaultApplicationDelegate, legacyTestRunner
})
promise.then(exitWithStatusCode) if getWindowLoadSettings().headless
promise.then (statusCode) ->
exitWithStatusCode(statusCode) if getWindowLoadSettings().headless
catch error
if getWindowLoadSettings().headless
console.error(error.stack ? error)

40
src/ipc-helpers.js Normal file
View File

@@ -0,0 +1,40 @@
var ipcRenderer = null
var ipcMain = null
var BrowserWindow = null
exports.call = function (methodName, ...args) {
if (!ipcRenderer) {
ipcRenderer = require('electron').ipcRenderer
}
var responseChannel = getResponseChannel(methodName)
return new Promise(function (resolve) {
ipcRenderer.on(responseChannel, function (event, result) {
ipcRenderer.removeAllListeners(responseChannel)
resolve(result)
})
ipcRenderer.send(methodName, ...args)
})
}
exports.respondTo = function (methodName, callback) {
if (!ipcMain) {
var electron = require('electron')
ipcMain = electron.ipcMain
BrowserWindow = electron.BrowserWindow
}
var responseChannel = getResponseChannel(methodName)
ipcMain.on(methodName, function (event, ...args) {
var browserWindow = BrowserWindow.fromWebContents(event.sender)
var result = callback(browserWindow, ...args)
event.sender.send(responseChannel, result)
})
}
function getResponseChannel (methodName) {
return 'ipc-helpers-' + methodName + '-response'
}

View File

@@ -10,6 +10,7 @@ class LanguageMode
# editor - The {TextEditor} to associate with
constructor: (@editor, @config) ->
{@buffer} = @editor
@regexesByPattern = {}
destroy: ->
@@ -144,13 +145,11 @@ class LanguageMode
if bufferRow > 0
for currentRow in [bufferRow-1..0] by -1
break if @buffer.isRowBlank(currentRow)
break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
startRow = currentRow
if bufferRow < @buffer.getLastRow()
for currentRow in [bufferRow+1..@buffer.getLastRow()] by 1
break if @buffer.isRowBlank(currentRow)
break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
endRow = currentRow
@@ -326,7 +325,8 @@ class LanguageMode
getRegexForProperty: (scopeDescriptor, property) ->
if pattern = @config.get(property, scope: scopeDescriptor)
new OnigRegExp(pattern)
@regexesByPattern[pattern] ?= new OnigRegExp(pattern)
@regexesByPattern[pattern]
increaseIndentRegexForScopeDescriptor: (scopeDescriptor) ->
@getRegexForProperty(scopeDescriptor, 'editor.increaseIndentPattern')

View File

@@ -1,7 +1,7 @@
path = require 'path'
_ = require 'underscore-plus'
ipc = require 'ipc'
{ipcRenderer} = require 'electron'
CSON = require 'season'
fs = require 'fs-plus'
{Disposable} = require 'event-kit'
@@ -191,7 +191,7 @@ class MenuManager
sendToBrowserProcess: (template, keystrokesByCommand) ->
keystrokesByCommand = @filterMultipleKeystroke(keystrokesByCommand)
ipc.send 'update-application-menu', template, keystrokesByCommand
ipcRenderer.send 'update-application-menu', template, keystrokesByCommand
# Get an {Array} of {String} classes for the given element.
classesForElement: (element) ->

View File

@@ -208,7 +208,7 @@ registerBuiltins = (devMode) ->
cache.builtins[builtin] = path.join(commonRoot, "#{builtin}.js")
rendererRoot = path.join(atomShellRoot, 'renderer', 'api', 'lib')
rendererBuiltins = ['ipc', 'remote']
rendererBuiltins = ['ipc-renderer', 'remote']
for builtin in rendererBuiltins
cache.builtins[builtin] = path.join(rendererRoot, "#{builtin}.js")

View File

@@ -357,7 +357,9 @@ class PackageManager
packagePaths = @getAvailablePackagePaths()
packagePaths = packagePaths.filter (packagePath) => not @isPackageDisabled(path.basename(packagePath))
packagePaths = _.uniq packagePaths, (packagePath) -> path.basename(packagePath)
@loadPackage(packagePath) for packagePath in packagePaths
@config.transact =>
@loadPackage(packagePath) for packagePath in packagePaths
return
@emitter.emit 'did-load-initial-packages'
loadPackage: (nameOrPath) ->
@@ -467,6 +469,14 @@ class PackageManager
return unless hook? and _.isString(hook) and hook.length > 0
@activationHookEmitter.on(hook, callback)
serialize: ->
for pack in @getActivePackages()
@serializePackage(pack)
@packageStates
serializePackage: (pack) ->
@setPackageState(pack.name, state) if state = pack.serialize?()
# Deactivate all packages
deactivatePackages: ->
@config.transact =>
@@ -478,8 +488,7 @@ class PackageManager
# Deactivate the package with the given name
deactivatePackage: (name) ->
pack = @getLoadedPackage(name)
if @isPackageActive(name)
@setPackageState(pack.name, state) if state = pack.serialize?()
@serializePackage(pack) if @isPackageActive(pack.name)
pack.deactivate()
delete @activePackages[pack.name]
delete @activatingPackages[pack.name]
@@ -532,11 +541,12 @@ class PackageManager
unless typeof metadata.name is 'string' and metadata.name.length > 0
metadata.name = packageName
if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string'
metadata.repository.url = metadata.repository.url.replace(/(^git\+)|(\.git$)/g, '')
metadata
normalizePackageMetadata: (metadata) ->
unless metadata?._id
normalizePackageData ?= require 'normalize-package-data'
normalizePackageData(metadata)
if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string'
metadata.repository.url = metadata.repository.url.replace(/^git\+/, '')

View File

@@ -84,7 +84,7 @@ class Package
@loadKeymaps()
@loadMenus()
@loadStylesheets()
@loadDeserializers()
@registerDeserializerMethods()
@configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata()
@settingsPromise = @loadSettings()
if @shouldRequireMainModuleOnLoad() and not @mainModule?
@@ -277,24 +277,24 @@ class Package
@stylesheets = @getStylesheetPaths().map (stylesheetPath) =>
[stylesheetPath, @themeManager.loadStylesheet(stylesheetPath, true)]
loadDeserializers: ->
registerDeserializerMethods: ->
if @metadata.deserializers?
for name, implementationPath of @metadata.deserializers
do =>
deserializePath = path.join(@path, implementationPath)
deserializeFunction = null
atom.deserializers.add
name: name,
deserialize: =>
@registerViewProviders()
deserializeFunction ?= require(deserializePath)
deserializeFunction.apply(this, arguments)
Object.keys(@metadata.deserializers).forEach (deserializerName) =>
methodName = @metadata.deserializers[deserializerName]
atom.deserializers.add
name: deserializerName,
deserialize: (state, atomEnvironment) =>
@registerViewProviders()
@requireMainModule()
@mainModule[methodName](state, atomEnvironment)
return
registerViewProviders: ->
if @metadata.viewProviders? and not @registeredViewProviders
for implementationPath in @metadata.viewProviders
@viewRegistry.addViewProvider(require(path.join(@path, implementationPath)))
@requireMainModule()
@metadata.viewProviders.forEach (methodName) =>
@viewRegistry.addViewProvider (model) =>
@mainModule[methodName](model)
@registeredViewProviders = true
getStylesheetsPath: ->

View File

@@ -1,5 +1,6 @@
Grim = require 'grim'
{find, compact, extend, last} = require 'underscore-plus'
{Emitter} = require 'event-kit'
{CompositeDisposable, Emitter} = require 'event-kit'
Model = require './model'
PaneAxis = require './pane-axis'
TextEditor = require './text-editor'
@@ -8,6 +9,11 @@ TextEditor = require './text-editor'
# Panes can contain multiple items, one of which is *active* at a given time.
# The view corresponding to the active item is displayed in the interface. In
# the default configuration, tabs are also displayed for each item.
#
# Each pane may also contain one *pending* item. When a pending item is added
# to a pane, it will replace the currently pending item, if any, instead of
# simply being added. In the default configuration, the text in the tab for
# pending items is shown in italics.
module.exports =
class Pane extends Model
container: undefined
@@ -15,7 +21,7 @@ class Pane extends Model
focused: false
@deserialize: (state, {deserializers, applicationDelegate, config, notifications}) ->
{items, activeItemURI, activeItemUri} = state
{items, itemStackIndices, activeItemURI, activeItemUri} = state
activeItemURI ?= activeItemUri
state.items = compact(items.map (itemState) -> deserializers.deserialize(itemState))
state.activeItem = find state.items, (item) ->
@@ -37,20 +43,25 @@ class Pane extends Model
} = params
@emitter = new Emitter
@itemSubscriptions = new WeakMap
@subscriptionsPerItem = new WeakMap
@items = []
@itemStack = []
@addItems(compact(params?.items ? []))
@setActiveItem(@items[0]) unless @getActiveItem()?
@addItemsToStack(params?.itemStackIndices ? [])
@setFlexScale(params?.flexScale ? 1)
serialize: ->
if typeof @activeItem?.getURI is 'function'
activeItemURI = @activeItem.getURI()
itemsToBeSerialized = compact(@items.map((item) -> item if typeof item.serialize is 'function'))
itemStackIndices = (itemsToBeSerialized.indexOf(item) for item in @itemStack when typeof item.serialize is 'function')
deserializer: 'Pane'
id: @id
items: compact(@items.map((item) -> item.serialize?()))
items: itemsToBeSerialized.map((item) -> item.serialize())
itemStackIndices: itemStackIndices
activeItemURI: activeItemURI
focused: @focused
flexScale: @flexScale
@@ -260,8 +271,8 @@ class Pane extends Model
getPanes: -> [this]
unsubscribeFromItem: (item) ->
@itemSubscriptions.get(item)?.dispose()
@itemSubscriptions.delete(item)
@subscriptionsPerItem.get(item)?.dispose()
@subscriptionsPerItem.delete(item)
###
Section: Items
@@ -278,12 +289,30 @@ class Pane extends Model
# Returns a pane item.
getActiveItem: -> @activeItem
setActiveItem: (activeItem) ->
setActiveItem: (activeItem, options) ->
{modifyStack} = options if options?
unless activeItem is @activeItem
@addItemToStack(activeItem) unless modifyStack is false
@activeItem = activeItem
@emitter.emit 'did-change-active-item', @activeItem
@activeItem
# Build the itemStack after deserializing
addItemsToStack: (itemStackIndices) ->
if @items.length > 0
if itemStackIndices.length is 0 or itemStackIndices.length isnt @items.length or itemStackIndices.indexOf(-1) >= 0
itemStackIndices = (i for i in [0..@items.length-1])
for itemIndex in itemStackIndices
@addItemToStack(@items[itemIndex])
return
# Add item (or move item) to the end of the itemStack
addItemToStack: (newItem) ->
return unless newItem?
index = @itemStack.indexOf(newItem)
@itemStack.splice(index, 1) unless index is -1
@itemStack.push(newItem)
# Return an {TextEditor} if the pane item is an {TextEditor}, or null otherwise.
getActiveEditor: ->
@activeItem if @activeItem instanceof TextEditor
@@ -296,6 +325,29 @@ class Pane extends Model
itemAtIndex: (index) ->
@items[index]
# Makes the next item in the itemStack active.
activateNextRecentlyUsedItem: ->
if @items.length > 1
@itemStackIndex = @itemStack.length - 1 unless @itemStackIndex?
@itemStackIndex = @itemStack.length if @itemStackIndex is 0
@itemStackIndex = @itemStackIndex - 1
nextRecentlyUsedItem = @itemStack[@itemStackIndex]
@setActiveItem(nextRecentlyUsedItem, modifyStack: false)
# Makes the previous item in the itemStack active.
activatePreviousRecentlyUsedItem: ->
if @items.length > 1
if @itemStackIndex + 1 is @itemStack.length or not @itemStackIndex?
@itemStackIndex = -1
@itemStackIndex = @itemStackIndex + 1
previousRecentlyUsedItem = @itemStack[@itemStackIndex]
@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)
# Public: Makes the next item active.
activateNextItem: ->
index = @getActiveItemIndex()
@@ -342,43 +394,81 @@ class Pane extends Model
# Public: Make the given item *active*, causing it to be displayed by
# the pane's view.
activateItem: (item) ->
#
# * `options` (optional) {Object}
# * `pending` (optional) {Boolean} indicating that the item should be added
# in a pending state if it does not yet exist in the pane. Existing pending
# items in a pane are replaced with new pending items when they are opened.
activateItem: (item, options={}) ->
if item?
if @activeItem?.isPending?()
if @getPendingItem() is @activeItem
index = @getActiveItemIndex()
else
index = @getActiveItemIndex() + 1
@addItem(item, index, false)
@addItem(item, extend({}, options, {index: index}))
@setActiveItem(item)
# Public: Add the given item to the pane.
#
# * `item` The item to add. It can be a model with an associated view or a
# view.
# * `index` (optional) {Number} indicating the index at which to add the item.
# If omitted, the item is added after the current active item.
# * `options` (optional) {Object}
# * `index` (optional) {Number} indicating the index at which to add the item.
# If omitted, the item is added after the current active item.
# * `pending` (optional) {Boolean} indicating that the item should be
# added in a pending state. Existing pending items in a pane are replaced with
# new pending items when they are opened.
#
# Returns the added item.
addItem: (item, index=@getActiveItemIndex() + 1, moved=false) ->
addItem: (item, options={}) ->
# Backward compat with old API:
# addItem(item, index=@getActiveItemIndex() + 1)
if typeof options is "number"
Grim.deprecate("Pane::addItem(item, #{options}) is deprecated in favor of Pane::addItem(item, {index: #{options}})")
options = index: options
index = options.index ? @getActiveItemIndex() + 1
moved = options.moved ? false
pending = options.pending ? false
throw new Error("Pane items must be objects. Attempted to add item #{item}.") unless item? and typeof item is 'object'
throw new Error("Adding a pane item with URI '#{item.getURI?()}' that has already been destroyed") if item.isDestroyed?()
return if item in @items
if item.isPending?()
for existingItem, i in @items
if existingItem.isPending?()
@destroyItem(existingItem)
break
if typeof item.onDidDestroy is 'function'
@itemSubscriptions.set item, item.onDidDestroy => @removeItem(item, false)
itemSubscriptions = new CompositeDisposable
itemSubscriptions.add item.onDidDestroy => @removeItem(item, false)
if typeof item.onDidTerminatePendingState is "function"
itemSubscriptions.add item.onDidTerminatePendingState =>
@clearPendingItem() if @getPendingItem() is item
itemSubscriptions.add item.onDidDestroy => @removeItem(item, false)
@subscriptionsPerItem.set item, itemSubscriptions
@items.splice(index, 0, item)
lastPendingItem = @getPendingItem()
@setPendingItem(item) if pending
@emitter.emit 'did-add-item', {item, index, moved}
@destroyItem(lastPendingItem) if lastPendingItem? and not moved
@setActiveItem(item) unless @getActiveItem()?
item
setPendingItem: (item) =>
if @pendingItem isnt item
mostRecentPendingItem = @pendingItem
@pendingItem = item
@emitter.emit 'item-did-terminate-pending-state', mostRecentPendingItem
getPendingItem: =>
@pendingItem or null
clearPendingItem: =>
@setPendingItem(null)
onItemDidTerminatePendingState: (callback) =>
@emitter.on 'item-did-terminate-pending-state', callback
# Public: Add the given items to the pane.
#
# * `items` An {Array} of items to add. Items can be views or models with
@@ -390,13 +480,14 @@ class Pane extends Model
# Returns an {Array} of added items.
addItems: (items, index=@getActiveItemIndex() + 1) ->
items = items.filter (item) => not (item in @items)
@addItem(item, index + i, false) for item, i in items
@addItem(item, {index: index + i}) for item, i in items
items
removeItem: (item, moved) ->
index = @items.indexOf(item)
return if index is -1
@pendingItem = null if @getPendingItem() is item
@removeItemFromStack(item)
@emitter.emit 'will-remove-item', {item, index, destroyed: not moved, moved}
@unsubscribeFromItem(item)
@@ -412,6 +503,14 @@ class Pane extends Model
@container?.didDestroyPaneItem({item, index, pane: this}) unless moved
@destroy() if @items.length is 0 and @config.get('core.destroyEmptyPanes')
# Remove the given item from the itemStack.
#
# * `item` The item to remove.
# * `index` {Number} indicating the index to which to remove the item from the itemStack.
removeItemFromStack: (item) ->
index = @itemStack.indexOf(item)
@itemStack.splice(index, 1) unless index is -1
# Public: Move the given item to the given index.
#
# * `item` The item to move.
@@ -430,7 +529,7 @@ class Pane extends Model
# given pane.
moveItemToPane: (item, pane, index) ->
@removeItem(item, true)
pane.addItem(item, index, true)
pane.addItem(item, {index: index, moved: true})
# Public: Destroy the active item and activate the next item.
destroyActiveItem: ->
@@ -661,7 +760,7 @@ class Pane extends Model
@parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this], @flexScale}))
@setFlexScale(1)
newPane = new Pane(extend({@applicationDelegate, @deserializerManager, @config}, params))
newPane = new Pane(extend({@applicationDelegate, @notificationManager, @deserializerManager, @config}, params))
switch side
when 'before' then @parent.insertChildBefore(this, newPane)
when 'after' then @parent.insertChildAfter(this, newPane)
@@ -713,7 +812,7 @@ class Pane extends Model
if @parent.orientation is 'vertical'
bottommostSibling = last(@parent.children)
if bottommostSibling instanceof PaneAxis
@splitRight()
@splitDown()
else
bottommostSibling
else

View File

@@ -54,8 +54,9 @@ class Project extends Model
Section: Serialization
###
deserialize: (state, deserializerManager) ->
deserialize: (state) ->
state.paths = [state.path] if state.path? # backward compatibility
state.paths = state.paths.filter (directoryPath) -> fs.isDirectorySync(directoryPath)
@buffers = _.compact state.buffers.map (bufferState) ->
# Check that buffer's file path is accessible
@@ -65,15 +66,15 @@ class Project extends Model
fs.closeSync(fs.openSync(bufferState.filePath, 'r'))
catch error
return unless error.code is 'ENOENT'
deserializerManager.deserialize(bufferState)
TextBuffer.deserialize(bufferState)
@subscribeToBuffer(buffer) for buffer in @buffers
@setPaths(state.paths)
serialize: ->
serialize: (options={}) ->
deserializer: 'Project'
paths: @getPaths()
buffers: _.compact(@buffers.map (buffer) -> buffer.serialize() if buffer.isRetained())
buffers: _.compact(@buffers.map (buffer) -> buffer.serialize({markerLayers: options.isUnloading is true}) if buffer.isRetained())
###
Section: Event Subscription

View File

@@ -1,7 +1,10 @@
ipc = require 'ipc'
{ipcRenderer} = require 'electron'
module.exports = ({commandRegistry, commandInstaller, config}) ->
commandRegistry.add 'atom-workspace',
'pane:show-next-recently-used-item': -> @getModel().getActivePane().activateNextRecentlyUsedItem()
'pane:show-previous-recently-used-item': -> @getModel().getActivePane().activatePreviousRecentlyUsedItem()
'pane:move-active-item-to-top-of-stack': -> @getModel().getActivePane().moveActiveItemToTopOfStack()
'pane:show-next-item': -> @getModel().getActivePane().activateNextItem()
'pane:show-previous-item': -> @getModel().getActivePane().activatePreviousItem()
'pane:show-item-1': -> @getModel().getActivePane().activateItemAtIndex(0)
@@ -18,30 +21,30 @@ module.exports = ({commandRegistry, commandInstaller, config}) ->
'window:increase-font-size': -> @getModel().increaseFontSize()
'window:decrease-font-size': -> @getModel().decreaseFontSize()
'window:reset-font-size': -> @getModel().resetFontSize()
'application:about': -> ipc.send('command', 'application:about')
'application:show-preferences': -> ipc.send('command', 'application:show-settings')
'application:show-settings': -> ipc.send('command', 'application:show-settings')
'application:quit': -> ipc.send('command', 'application:quit')
'application:hide': -> ipc.send('command', 'application:hide')
'application:hide-other-applications': -> ipc.send('command', 'application:hide-other-applications')
'application:install-update': -> ipc.send('command', 'application:install-update')
'application:unhide-all-applications': -> ipc.send('command', 'application:unhide-all-applications')
'application:new-window': -> ipc.send('command', 'application:new-window')
'application:new-file': -> ipc.send('command', 'application:new-file')
'application:open': -> ipc.send('command', 'application:open')
'application:open-file': -> ipc.send('command', 'application:open-file')
'application:open-folder': -> ipc.send('command', 'application:open-folder')
'application:open-dev': -> ipc.send('command', 'application:open-dev')
'application:open-safe': -> ipc.send('command', 'application:open-safe')
'application:about': -> ipcRenderer.send('command', 'application:about')
'application:show-preferences': -> ipcRenderer.send('command', 'application:show-settings')
'application:show-settings': -> ipcRenderer.send('command', 'application:show-settings')
'application:quit': -> ipcRenderer.send('command', 'application:quit')
'application:hide': -> ipcRenderer.send('command', 'application:hide')
'application:hide-other-applications': -> ipcRenderer.send('command', 'application:hide-other-applications')
'application:install-update': -> ipcRenderer.send('command', 'application:install-update')
'application:unhide-all-applications': -> ipcRenderer.send('command', 'application:unhide-all-applications')
'application:new-window': -> ipcRenderer.send('command', 'application:new-window')
'application:new-file': -> ipcRenderer.send('command', 'application:new-file')
'application:open': -> ipcRenderer.send('command', 'application:open')
'application:open-file': -> ipcRenderer.send('command', 'application:open-file')
'application:open-folder': -> ipcRenderer.send('command', 'application:open-folder')
'application:open-dev': -> ipcRenderer.send('command', 'application:open-dev')
'application:open-safe': -> ipcRenderer.send('command', 'application:open-safe')
'application:add-project-folder': -> atom.addProjectFolder()
'application:minimize': -> ipc.send('command', 'application:minimize')
'application:zoom': -> ipc.send('command', 'application:zoom')
'application:bring-all-windows-to-front': -> ipc.send('command', 'application:bring-all-windows-to-front')
'application:open-your-config': -> ipc.send('command', 'application:open-your-config')
'application:open-your-init-script': -> ipc.send('command', 'application:open-your-init-script')
'application:open-your-keymap': -> ipc.send('command', 'application:open-your-keymap')
'application:open-your-snippets': -> ipc.send('command', 'application:open-your-snippets')
'application:open-your-stylesheet': -> ipc.send('command', 'application:open-your-stylesheet')
'application:minimize': -> ipcRenderer.send('command', 'application:minimize')
'application:zoom': -> ipcRenderer.send('command', 'application:zoom')
'application:bring-all-windows-to-front': -> ipcRenderer.send('command', 'application:bring-all-windows-to-front')
'application:open-your-config': -> ipcRenderer.send('command', 'application:open-your-config')
'application:open-your-init-script': -> ipcRenderer.send('command', 'application:open-your-init-script')
'application:open-your-keymap': -> ipcRenderer.send('command', 'application:open-your-keymap')
'application:open-your-snippets': -> ipcRenderer.send('command', 'application:open-your-snippets')
'application:open-your-stylesheet': -> ipcRenderer.send('command', 'application:open-your-stylesheet')
'application:open-license': -> @getModel().openLicense()
'window:run-package-specs': -> @runPackageSpecs()
'window:focus-next-pane': -> @getModel().activateNextPane()

View File

@@ -1,6 +1,6 @@
# Using clipboard in renderer process is not safe on Linux.
module.exports =
if process.platform is 'linux' and process.type is 'renderer'
require('remote').require('clipboard')
require('electron').remote.clipboard
else
require('clipboard')
require('electron').clipboard

View File

@@ -378,7 +378,7 @@ class Selection extends Model
indentAdjustment = @editor.indentLevelForLine(precedingText) - options.indentBasis
@adjustIndent(remainingLines, indentAdjustment)
if options.autoIndent and not NonWhitespaceRegExp.test(precedingText) and remainingLines.length > 0
if options.autoIndent and NonWhitespaceRegExp.test(text) and not NonWhitespaceRegExp.test(precedingText) and remainingLines.length > 0
autoIndentFirstLine = true
firstLine = precedingText + firstInsertedLine
desiredIndentLevel = @editor.languageMode.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine)
@@ -749,7 +749,7 @@ class Selection extends Model
#
# * `otherSelection` A {Selection} to compare against
compare: (otherSelection) ->
@getBufferRange().compare(otherSelection.getBufferRange())
@marker.compare(otherSelection.marker)
###
Section: Private Utilities
@@ -804,11 +804,11 @@ class Selection extends Model
@wordwise = false
@linewise = false
autoscroll: ->
autoscroll: (options) ->
if @marker.hasTail()
@editor.scrollToScreenRange(@getScreenRange(), reversed: @isReversed())
@editor.scrollToScreenRange(@getScreenRange(), Object.assign({reversed: @isReversed()}, options))
else
@cursor.autoscroll()
@cursor.autoscroll(options)
clearAutoscroll: ->

95
src/state-store.js Normal file
View File

@@ -0,0 +1,95 @@
'use strict'
module.exports =
class StateStore {
constructor (databaseName, version) {
this.dbPromise = new Promise((resolve) => {
let dbOpenRequest = indexedDB.open(databaseName, version)
dbOpenRequest.onupgradeneeded = (event) => {
let db = event.target.result
db.createObjectStore('states')
}
dbOpenRequest.onsuccess = () => {
resolve(dbOpenRequest.result)
}
dbOpenRequest.onerror = (error) => {
console.error('Could not connect to indexedDB', error)
resolve(null)
}
})
}
connect () {
return this.dbPromise.then(db => !!db)
}
save (key, value) {
return new Promise((resolve, reject) => {
this.dbPromise.then(db => {
if (db == null) resolve()
var request = db.transaction(['states'], 'readwrite')
.objectStore('states')
.put({value: value, storedAt: new Date().toString()}, key)
request.onsuccess = resolve
request.onerror = reject
})
})
}
load (key) {
return this.dbPromise.then(db => {
if (!db) return
return new Promise((resolve, reject) => {
var request = db.transaction(['states'])
.objectStore('states')
.get(key)
request.onsuccess = (event) => {
let result = event.target.result
if (result && !result.isJSON) {
resolve(result.value)
} else {
resolve(null)
}
}
request.onerror = (event) => reject(event)
})
})
}
clear () {
return this.dbPromise.then(db => {
if (!db) return
return new Promise((resolve, reject) => {
var request = db.transaction(['states'], 'readwrite')
.objectStore('states')
.clear()
request.onsuccess = resolve
request.onerror = reject
})
})
}
count () {
return this.dbPromise.then(db => {
if (!db) return
return new Promise((resolve, reject) => {
var request = db.transaction(['states'])
.objectStore('states')
.count()
request.onsuccess = () => {
resolve(request.result)
}
request.onerror = reject
})
})
}
}

View File

@@ -6,7 +6,7 @@ class StorageFolder
constructor: (containingPath) ->
@path = path.join(containingPath, "storage") if containingPath?
store: (name, object) ->
storeSync: (name, object) ->
return unless @path?
fs.writeFileSync(@pathForKey(name), JSON.stringify(object), 'utf8')

View File

@@ -2,7 +2,7 @@ _ = require 'underscore-plus'
scrollbarStyle = require 'scrollbar-style'
{Range, Point} = require 'text-buffer'
{CompositeDisposable} = require 'event-kit'
ipc = require 'ipc'
{ipcRenderer} = require 'electron'
TextEditorPresenter = require './text-editor-presenter'
GutterContainerComponent = require './gutter-container-component'
@@ -43,7 +43,7 @@ class TextEditorComponent
@assert domNode?, "TextEditorComponent::domNode was set to null."
@domNodeValue = domNode
constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, tileSize, @views, @themes, @config, @workspace, @assert, @grammars}) ->
constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, tileSize, @views, @themes, @config, @workspace, @assert, @grammars, scrollPastEnd}) ->
@tileSize = tileSize if tileSize?
@disposables = new CompositeDisposable
@@ -61,6 +61,7 @@ class TextEditorComponent
stoppedScrollingDelay: 200
config: @config
lineTopIndex: lineTopIndex
scrollPastEnd: scrollPastEnd
@presenter.onDidUpdateState(@requestUpdate)
@@ -279,10 +280,10 @@ class TextEditorComponent
writeSelectedTextToSelectionClipboard = =>
return if @editor.isDestroyed()
if selectedText = @editor.getSelectedText()
# This uses ipc.send instead of clipboard.writeText because
# clipboard.writeText is a sync ipc call on Linux and that
# This uses ipcRenderer.send instead of clipboard.writeText because
# clipboard.writeText is a sync ipcRenderer call on Linux and that
# will slow down selections.
ipc.send('write-text-to-selection-clipboard', selectedText)
ipcRenderer.send('write-text-to-selection-clipboard', selectedText)
@disposables.add @editor.onDidChangeSelectionRange ->
clearTimeout(timeoutId)
timeoutId = setTimeout(writeSelectedTextToSelectionClipboard)

View File

@@ -17,6 +17,8 @@ class TextEditorElement extends HTMLElement
focusOnAttach: false
hasTiledRendering: true
logicalDisplayBuffer: true
scrollPastEnd: true
autoHeight: true
createdCallback: ->
# Use globals when the following instance variables aren't set.
@@ -38,6 +40,9 @@ class TextEditorElement extends HTMLElement
@setAttribute('tabindex', -1)
initializeContent: (attributes) ->
unless @autoHeight
@style.height = "100%"
if @config.get('editor.useShadowDOM')
@useShadowDOM = true
@@ -86,7 +91,7 @@ class TextEditorElement extends HTMLElement
@subscriptions.add @component.onDidChangeScrollLeft =>
@emitter.emit("did-change-scroll-left", arguments...)
initialize: (model, {@views, @config, @themes, @workspace, @assert, @styles, @grammars}) ->
initialize: (model, {@views, @config, @themes, @workspace, @assert, @styles, @grammars}, @autoHeight = true, @scrollPastEnd = true) ->
throw new Error("Must pass a config parameter when initializing TextEditorElements") unless @views?
throw new Error("Must pass a config parameter when initializing TextEditorElements") unless @config?
throw new Error("Must pass a themes parameter when initializing TextEditorElements") unless @themes?
@@ -143,6 +148,7 @@ class TextEditorElement extends HTMLElement
workspace: @workspace
assert: @assert
grammars: @grammars
scrollPastEnd: @scrollPastEnd
)
@rootElement.appendChild(@component.getDomNode())

View File

@@ -14,7 +14,7 @@ class TextEditorPresenter
minimumReflowInterval: 200
constructor: (params) ->
{@model, @config, @lineTopIndex} = params
{@model, @config, @lineTopIndex, scrollPastEnd} = params
{@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @tileSize} = params
{@contentFrameWidth} = params
@tokenIterator = @model.displayLayer.buildTokenIterator()
@@ -47,6 +47,8 @@ class TextEditorPresenter
@startReflowing() if @continuousReflow
@updating = false
@scrollPastEndOverride = scrollPastEnd ? true
setLinesYardstick: (@linesYardstick) ->
getLinesYardstick: -> @linesYardstick
@@ -432,18 +434,14 @@ class TextEditorPresenter
return
updateCursorsState: ->
@state.content.cursors = {}
@updateCursorState(cursor) for cursor in @model.cursors # using property directly to avoid allocation
return
updateCursorState: (cursor) ->
return unless @startRow? and @endRow? and @hasPixelRectRequirements() and @baseCharacterWidth?
screenRange = cursor.getScreenRange()
return unless cursor.isVisible() and @startRow <= screenRange.start.row < @endRow
pixelRect = @pixelRectForScreenRange(screenRange)
pixelRect.width = Math.round(@baseCharacterWidth) if pixelRect.width is 0
@state.content.cursors[cursor.id] = pixelRect
@state.content.cursors = {}
for cursor in @model.cursorsForScreenRowRange(@startRow, @endRow - 1) when cursor.isVisible()
pixelRect = @pixelRectForScreenRange(cursor.getScreenRange())
pixelRect.width = Math.round(@baseCharacterWidth) if pixelRect.width is 0
@state.content.cursors[cursor.id] = pixelRect
return
updateOverlaysState: ->
return unless @hasOverlayPositionRequirements()
@@ -603,7 +601,14 @@ class TextEditorPresenter
if endRow > startRow
bufferRows = @model.bufferRowsForScreenRows(startRow, endRow - 1)
previousBufferRow = -1
foldable = false
for bufferRow, i in bufferRows
# don't compute foldability more than once per buffer row
if previousBufferRow isnt bufferRow
foldable = @model.isFoldableAtBufferRow(bufferRow)
previousBufferRow = bufferRow
if bufferRow is lastBufferRow
softWrapped = true
else
@@ -613,7 +618,6 @@ class TextEditorPresenter
screenRow = startRow + i
lineId = @lineIdForScreenRow(screenRow)
decorationClasses = @lineNumberDecorationClassesForRow(screenRow)
foldable = @model.isFoldableAtScreenRow(screenRow)
blockDecorationsBeforeCurrentScreenRowHeight = @lineTopIndex.pixelPositionAfterBlocksForRow(screenRow) - @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRow)
blockDecorationsHeight = blockDecorationsBeforeCurrentScreenRowHeight
if screenRow % @tileSize isnt 0
@@ -659,7 +663,7 @@ class TextEditorPresenter
return unless @contentHeight? and @clientHeight?
contentHeight = @contentHeight
if @scrollPastEnd
if @scrollPastEnd and @scrollPastEndOverride
extraScrollHeight = @clientHeight - (@lineHeight * 3)
contentHeight += extraScrollHeight if extraScrollHeight > 0
scrollHeight = Math.max(contentHeight, @height)

View File

@@ -0,0 +1,40 @@
{Emitter, Disposable} = require 'event-kit'
# Experimental: This global registry tracks registered `TextEditors`.
#
# If you want to add functionality to a wider set of text editors than just
# those appearing within workspace panes, use `atom.textEditors.observe` to
# invoke a callback for all current and future registered text editors.
#
# If you want packages to be able to add functionality to your non-pane text
# editors (such as a search field in a custom user interface element), register
# them for observation via `atom.textEditors.add`. **Important:** When you're
# done using your editor, be sure to call `dispose` on the returned disposable
# to avoid leaking editors.
module.exports =
class TextEditorRegistry
constructor: ->
@editors = new Set
@emitter = new Emitter
# Register a `TextEditor`.
#
# * `editor` The editor to register.
#
# Returns a {Disposable} on which `.dispose()` can be called to remove the
# added editor. To avoid any memory leaks this should be called when the
# editor is destroyed.
add: (editor) ->
@editors.add(editor)
@emitter.emit 'did-add-editor', editor
new Disposable => @editors.delete(editor)
# Invoke the given callback with all the current and future registered
# `TextEditors`.
#
# * `callback` {Function} to be called with current and future text editors.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
observe: (callback) ->
@editors.forEach(callback)
@emitter.on 'did-add-editor', callback

View File

@@ -11,6 +11,7 @@ Selection = require './selection'
TextMateScopeSelector = require('first-mate').ScopeSelector
{Directory} = require "pathwatcher"
GutterContainer = require './gutter-container'
TextEditorElement = require './text-editor-element'
# Essential: This class represents all essential editing state for a single
# {TextBuffer}, including cursor and selection positions, folds, and soft wraps.
@@ -61,6 +62,10 @@ class TextEditor extends Model
suppressSelectionMerging: false
selectionFlashDuration: 500
gutterContainer: null
editorElement: null
Object.defineProperty @prototype, "element",
get: -> @getElement()
@deserialize: (state, atomEnvironment) ->
try
@@ -82,7 +87,10 @@ class TextEditor extends Model
state.project = atomEnvironment.project
state.assert = atomEnvironment.assert.bind(atomEnvironment)
state.applicationDelegate = atomEnvironment.applicationDelegate
new this(state)
editor = new this(state)
disposable = atomEnvironment.textEditors.add(editor)
editor.onDidDestroy -> disposable.dispose()
editor
constructor: (params={}) ->
super
@@ -92,7 +100,7 @@ class TextEditor extends Model
softWrapped, @displayBuffer, @selectionsMarkerLayer, buffer, suppressCursorCreation,
@mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config,
@notificationManager, @packageManager, @clipboard, @viewRegistry, @grammarRegistry,
@project, @assert, @applicationDelegate, @pending
@project, @assert, @applicationDelegate, grammar, showInvisibles, @autoHeight, @scrollPastEnd
} = params
throw new Error("Must pass a config parameter when constructing TextEditors") unless @config?
@@ -109,11 +117,17 @@ class TextEditor extends Model
@emitter = new Emitter
@disposables = new CompositeDisposable
@cursors = []
@cursorsByMarkerId = new Map
@selections = []
@autoHeight ?= true
@scrollPastEnd ?= true
@hasTerminatedPendingState = false
showInvisibles ?= true
buffer ?= new TextBuffer
@displayBuffer ?= new DisplayBuffer({
buffer, tabLength, softWrapped, ignoreInvisibles: @mini, largeFileMode,
buffer, tabLength, softWrapped, ignoreInvisibles: @mini or not showInvisibles, largeFileMode,
@config, @assert, @grammarRegistry, @packageManager
})
{@buffer, @displayLayer} = @displayBuffer
@@ -142,6 +156,9 @@ class TextEditor extends Model
priority: 0
visible: lineNumberGutterVisible
if grammar?
@setGrammar(grammar)
serialize: ->
deserializer: 'TextEditor'
id: @id
@@ -150,7 +167,6 @@ class TextEditor extends Model
firstVisibleScreenColumn: @getFirstVisibleScreenColumn()
displayBuffer: @displayBuffer.serialize()
selectionsMarkerLayerId: @selectionsMarkerLayer.id
pending: @isPending()
subscribeToBuffer: ->
@buffer.retain()
@@ -162,12 +178,18 @@ class TextEditor extends Model
@disposables.add @buffer.onDidChangeEncoding =>
@emitter.emit 'did-change-encoding', @getEncoding()
@disposables.add @buffer.onDidDestroy => @destroy()
if @pending
@disposables.add @buffer.onDidChangeModified =>
@terminatePendingState() if @buffer.isModified()
@disposables.add @buffer.onDidChangeModified =>
@terminatePendingState() if not @hasTerminatedPendingState and @buffer.isModified()
@preserveCursorPositionOnBufferReload()
terminatePendingState: ->
@emitter.emit 'did-terminate-pending-state' if not @hasTerminatedPendingState
@hasTerminatedPendingState = true
onDidTerminatePendingState: (callback) ->
@emitter.on 'did-terminate-pending-state', callback
subscribeToDisplayBuffer: ->
@disposables.add @selectionsMarkerLayer.onDidCreateMarker @addSelection.bind(this)
@disposables.add @displayBuffer.onDidChangeGrammar @handleGrammarChange.bind(this)
@@ -574,13 +596,6 @@ class TextEditor extends Model
getEditorWidthInChars: ->
@displayBuffer.getEditorWidthInChars()
onDidTerminatePendingState: (callback) ->
@emitter.on 'did-terminate-pending-state', callback
terminatePendingState: ->
return if not @pending
@pending = false
@emitter.emit 'did-terminate-pending-state'
###
Section: File Details
@@ -665,9 +680,6 @@ class TextEditor extends Model
# Essential: Returns {Boolean} `true` if this editor has no content.
isEmpty: -> @buffer.isEmpty()
# Returns {Boolean} `true` if this editor is pending and `false` if it is permanent.
isPending: -> Boolean(@pending)
# Copies the current file path to the native clipboard.
copyPathToClipboard: (relative = false) ->
if filePath = @getPath()
@@ -1954,10 +1966,18 @@ class TextEditor extends Model
getCursorsOrderedByBufferPosition: ->
@getCursors().sort (a, b) -> a.compare(b)
cursorsForScreenRowRange: (startScreenRow, endScreenRow) ->
cursors = []
for marker in @selectionsMarkerLayer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow])
if cursor = @cursorsByMarkerId.get(marker.id)
cursors.push(cursor)
cursors
# Add a cursor based on the given {DisplayMarker}.
addCursor: (marker) ->
cursor = new Cursor(editor: this, marker: marker, config: @config)
@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)
@@ -2456,6 +2476,7 @@ class TextEditor extends Model
removeSelection: (selection) ->
_.remove(@cursors, selection.cursor)
_.remove(@selections, selection)
@cursorsByMarkerId.delete(selection.cursor.marker.id)
@emitter.emit 'did-remove-cursor', selection.cursor
@emitter.emit 'did-remove-selection', selection
@@ -2470,6 +2491,7 @@ class TextEditor extends Model
selections = @getSelections()
if selections.length > 1
selection.destroy() for selection in selections[1...(selections.length)]
selections[0].autoscroll(center: true)
true
else
false
@@ -2924,6 +2946,7 @@ class TextEditor extends Model
# Extended: Unfold all existing folds.
unfoldAll: ->
@languageMode.unfoldAll()
@scrollToCursorPosition()
# Extended: Fold all foldable lines at the given indent level.
#
@@ -2939,8 +2962,7 @@ class TextEditor extends Model
#
# Returns a {Boolean}.
isFoldableAtBufferRow: (bufferRow) ->
# @languageMode.isFoldableAtBufferRow(bufferRow)
@displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow)?.foldable ? false
@displayBuffer.isFoldableAtBufferRow(bufferRow)
# Extended: Determine whether the given row in screen coordinates is foldable.
#
@@ -3119,6 +3141,10 @@ class TextEditor extends Model
Section: TextEditor Rendering
###
# Get the Element for the editor.
getElement: ->
@editorElement ?= new TextEditorElement().initialize(this, atom, @autoHeight, @scrollPastEnd)
# Essential: Retrieves the greyed out placeholder of a mini editor.
#
# Returns a {String}.
@@ -3192,8 +3218,8 @@ class TextEditor extends Model
# top of the visible area.
setFirstVisibleScreenRow: (screenRow, fromView) ->
unless fromView
maxScreenRow = @getLineCount() - 1
unless @config.get('editor.scrollPastEnd')
maxScreenRow = @getScreenLineCount() - 1
unless @config.get('editor.scrollPastEnd') and @scrollPastEnd
height = @displayBuffer.getHeight()
lineHeightInPixels = @displayBuffer.getLineHeightInPixels()
if height? and lineHeightInPixels?
@@ -3210,7 +3236,7 @@ class TextEditor extends Model
height = @displayBuffer.getHeight()
lineHeightInPixels = @displayBuffer.getLineHeightInPixels()
if height? and lineHeightInPixels?
Math.min(@firstVisibleScreenRow + Math.floor(height / lineHeightInPixels), @getLineCount() - 1)
Math.min(@firstVisibleScreenRow + Math.floor(height / lineHeightInPixels), @getScreenLineCount() - 1)
else
null

View File

@@ -220,8 +220,6 @@ class TokenizedBuffer extends Model
@validateRow(endRow)
@invalidateRow(endRow + 1) unless filledRegion
[startRow, endRow] = @updateFoldableStatus(startRow, endRow)
event = {start: startRow, end: endRow, delta: 0}
@emitter.emit 'did-change', event
@emitter.emit 'did-invalidate-range', Range(Point(startRow, 0), Point(endRow + 1, 0))
@@ -282,10 +280,8 @@ class TokenizedBuffer extends Model
if newEndStack and not _.isEqual(newEndStack, previousEndStack)
@invalidateRow(end + delta + 1)
[start, end] = @updateFoldableStatus(start, end + delta)
end -= delta
@invalidatedRange = Range(start, end)
event = {start, end, delta, bufferChange: e}
@emitter.emit 'did-change', event
@@ -299,23 +295,6 @@ class TokenizedBuffer extends Model
row - increment
updateFoldableStatus: (startRow, endRow) ->
return [startRow, endRow] if @largeFileMode
scanStartRow = @buffer.previousNonBlankRow(startRow) ? startRow
scanStartRow-- while scanStartRow > 0 and @tokenizedLineForRow(scanStartRow).isComment()
scanEndRow = @buffer.nextNonBlankRow(endRow) ? endRow
for row in [scanStartRow..scanEndRow] by 1
foldable = @isFoldableAtRow(row)
line = @tokenizedLineForRow(row)
unless line.foldable is foldable
line.foldable = foldable
startRow = Math.min(startRow, row)
endRow = Math.max(endRow, row)
[startRow, endRow]
isFoldableAtRow: (row) ->
if @largeFileMode
false

View File

@@ -33,7 +33,6 @@ class TokenizedLine
endOfLineInvisibles: null
lineIsWhitespaceOnly: false
firstNonWhitespaceIndex: 0
foldable: false
constructor: (properties) ->
@id = idCounter++
@@ -355,15 +354,19 @@ class TokenizedLine
@endOfLineInvisibles.push(eol) if eol
isComment: ->
return @isCommentLine if @isCommentLine?
@isCommentLine = false
iterator = @getTokenIterator()
while iterator.next()
scopes = iterator.getScopes()
continue if scopes.length is 1
continue unless NonWhitespaceRegex.test(iterator.getText())
for scope in scopes
return true if CommentScopeRegex.test(scope)
if CommentScopeRegex.test(scope)
@isCommentLine = true
break
break
false
@isCommentLine
isOnlyWhitespace: ->
@lineIsWhitespaceOnly

View File

@@ -63,6 +63,8 @@ class TooltipManager
# full list of options. You can also supply the following additional options:
# * `title` A {String} or {Function} to use for the text in the tip. If
# given a function, `this` will be set to the `target` element.
# * `trigger` A {String} that's the same as Bootstrap 'click | hover | focus
# | manual', except 'manual' will show the tooltip immediately.
# * `keyBindingCommand` A {String} containing a command name. If you specify
# this option and a key binding exists that matches the command, it will
# be appended to the title or rendered alone if no title is specified.

View File

@@ -64,7 +64,9 @@ Tooltip.prototype.init = function (element, options) {
if (trigger === 'click') {
this.disposables.add(listen(this.element, 'click', this.options.selector, this.toggle.bind(this)))
} else if (trigger !== 'manual') {
} else if (trigger === 'manual') {
this.show()
} else {
var eventIn, eventOut
if (trigger === 'hover') {

View File

@@ -171,6 +171,11 @@ class ViewRegistry
if object instanceof HTMLElement
return object
if typeof object?.getElement is 'function'
element = object.getElement()
if element instanceof HTMLElement
return element
if object?.element instanceof HTMLElement
return object.element

View File

@@ -15,7 +15,8 @@ class WindowEventHandler
@addEventListener(@window, 'focus', @handleWindowFocus)
@addEventListener(@window, 'blur', @handleWindowBlur)
@addEventListener(@document, 'keydown', @handleDocumentKeydown)
@addEventListener(@document, 'keyup', @handleDocumentKeyEvent)
@addEventListener(@document, 'keydown', @handleDocumentKeyEvent)
@addEventListener(@document, 'drop', @handleDocumentDrop)
@addEventListener(@document, 'dragover', @handleDocumentDragover)
@addEventListener(@document, 'contextmenu', @handleDocumentContextmenu)
@@ -66,7 +67,7 @@ class WindowEventHandler
target.addEventListener(eventName, handler)
@subscriptions.add(new Disposable(-> target.removeEventListener(eventName, handler)))
handleDocumentKeydown: (event) =>
handleDocumentKeyEvent: (event) =>
@atomEnvironment.keymaps.handleKeyboardEvent(event)
event.stopImmediatePropagation()
@@ -133,7 +134,7 @@ class WindowEventHandler
handleWindowBlur: =>
@document.body.classList.add('is-blurred')
@atomEnvironment.storeDefaultWindowDimensions()
@atomEnvironment.storeWindowDimensions()
handleWindowBeforeunload: =>
confirmed = @atomEnvironment.workspace?.confirmClose(windowCloseRequested: true)
@@ -141,7 +142,6 @@ class WindowEventHandler
@atomEnvironment.hide()
@reloadRequested = false
@atomEnvironment.storeDefaultWindowDimensions()
@atomEnvironment.storeWindowDimensions()
if confirmed
@atomEnvironment.unloadEditorWindow()

View File

@@ -1,19 +1,10 @@
remote = require 'remote'
{remote} = require 'electron'
_ = require 'underscore-plus'
windowLoadSettings = null
exports.getWindowLoadSettings = ->
windowLoadSettings ?= JSON.parse(window.decodeURIComponent(window.location.hash.substr(1)))
clone = _.deepClone(windowLoadSettings)
# The windowLoadSettings.windowState could be large, request it only when needed.
clone.__defineGetter__ 'windowState', ->
remote.getCurrentWindow().loadSettings.windowState
clone.__defineSetter__ 'windowState', (value) ->
remote.getCurrentWindow().loadSettings.windowState = value
clone
exports.setWindowLoadSettings = (settings) ->
windowLoadSettings = settings

View File

@@ -1,4 +1,4 @@
ipc = require 'ipc'
{ipcRenderer} = require 'electron'
path = require 'path'
{Disposable, CompositeDisposable} = require 'event-kit'
Grim = require 'grim'
@@ -117,6 +117,6 @@ class WorkspaceElement extends HTMLElement
[projectPath] = @project.relativizePath(activePath)
else
[projectPath] = @project.getPaths()
ipc.send('run-package-specs', path.join(projectPath, 'spec')) if projectPath
ipcRenderer.send('run-package-specs', path.join(projectPath, 'spec')) if projectPath
module.exports = WorkspaceElement = document.registerElement 'atom-workspace', prototype: WorkspaceElement.prototype

View File

@@ -43,6 +43,12 @@ class Workspace extends Model
@defaultDirectorySearcher = new DefaultDirectorySearcher()
@consumeServices(@packageManager)
# One cannot simply .bind here since it could be used as a component with
# Etch, in which case it'd be `new`d. And when it's `new`d, `this` is always
# the newly created object.
realThis = this
@buildTextEditor = -> Workspace.prototype.buildTextEditor.apply(realThis, arguments)
@panelContainers =
top: new PanelContainer({location: 'top'})
left: new PanelContainer({location: 'left'})
@@ -394,15 +400,18 @@ class Workspace extends Model
# initially. Defaults to `0`.
# * `initialColumn` A {Number} indicating which column to move the cursor to
# initially. Defaults to `0`.
# * `split` Either 'left', 'right', 'top' or 'bottom'.
# * `split` Either 'left', 'right', 'up' or 'down'.
# If 'left', the item will be opened in leftmost pane of the current active pane's row.
# If 'right', the item will be opened in the rightmost pane of the current active pane's row.
# If 'up', the item will be opened in topmost pane of the current active pane's row.
# If 'down', the item will be opened in the bottommost pane of the current active pane's row.
# If 'right', the item will be opened in the rightmost pane of the current active pane's row. If only one pane exists in the row, a new pane will be created.
# If 'up', the item will be opened in topmost pane of the current active pane's column.
# If 'down', the item will be opened in the bottommost pane of the current active pane's column. If only one pane exists in the column, a new pane will be created.
# * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on
# containing pane. Defaults to `true`.
# * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem}
# on containing pane. Defaults to `true`.
# * `pending` A {Boolean} indicating whether or not the item should be opened
# in a pending state. Existing pending items in a pane are replaced with
# new pending items when they are opened.
# * `searchAllPanes` A {Boolean}. If `true`, the workspace will attempt to
# activate an existing item for the given URI on any pane.
# If `false`, only the active pane will be searched for
@@ -414,6 +423,9 @@ class Workspace extends Model
split = options.split
uri = @project.resolvePath(uri)
if not atom.config.get('core.allowPendingPaneItems')
options.pending = false
# 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?
@@ -473,7 +485,8 @@ class Workspace extends Model
activateItem = options.activateItem ? true
if uri?
item = pane.itemForURI(uri)
if item = pane.itemForURI(uri)
pane.clearPendingItem() if not options.pending and pane.getPendingItem() is item
item ?= opener(uri, options) for opener in @getOpeners() when not item
try
@@ -496,7 +509,7 @@ class Workspace extends Model
return item if pane.isDestroyed()
@itemOpened(item)
pane.activateItem(item) if activateItem
pane.activateItem(item, {pending: options.pending}) if activateItem
pane.activate() if activatePane
initialLine = initialColumn = 0
@@ -551,7 +564,10 @@ class Workspace extends Model
@config, @notificationManager, @packageManager, @clipboard, @viewRegistry,
@grammarRegistry, @project, @assert, @applicationDelegate
}, params)
new TextEditor(params)
editor = new TextEditor(params)
disposable = atom.textEditors.add(editor)
editor.onDidDestroy -> disposable.dispose()
editor
# Public: Asynchronously reopens the last-closed item's URI if it hasn't already been
# reopened.