mirror of
https://github.com/atom/atom.git
synced 2026-04-28 03:01:47 -04:00
Merge branch 'master' of github.com:atom/atom into pr-11139/atom/ld-change-range-event
This commit is contained in:
@@ -1,246 +0,0 @@
|
||||
_ = require 'underscore-plus'
|
||||
{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) ->
|
||||
ipcRenderer.send('open', params)
|
||||
|
||||
pickFolder: (callback) ->
|
||||
responseChannel = "atom-pick-folder-response"
|
||||
ipcRenderer.on responseChannel, (event, path) ->
|
||||
ipcRenderer.removeAllListeners(responseChannel)
|
||||
callback(path)
|
||||
ipcRenderer.send("pick-folder", responseChannel)
|
||||
|
||||
getCurrentWindow: ->
|
||||
remote.getCurrentWindow()
|
||||
|
||||
closeWindow: ->
|
||||
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) ->
|
||||
ipcHelpers.call('set-window-size', width, height)
|
||||
|
||||
getWindowPosition: ->
|
||||
[x, y] = remote.getCurrentWindow().getPosition()
|
||||
{x, y}
|
||||
|
||||
setWindowPosition: (x, y) ->
|
||||
ipcHelpers.call('set-window-position', x, y)
|
||||
|
||||
centerWindow: ->
|
||||
ipcHelpers.call('center-window')
|
||||
|
||||
focusWindow: ->
|
||||
ipcHelpers.call('focus-window')
|
||||
|
||||
showWindow: ->
|
||||
ipcHelpers.call('show-window')
|
||||
|
||||
hideWindow: ->
|
||||
ipcHelpers.call('hide-window')
|
||||
|
||||
reloadWindow: ->
|
||||
ipcRenderer.send("call-window-method", "reload")
|
||||
|
||||
isWindowMaximized: ->
|
||||
remote.getCurrentWindow().isMaximized()
|
||||
|
||||
maximizeWindow: ->
|
||||
ipcRenderer.send("call-window-method", "maximize")
|
||||
|
||||
isWindowFullScreen: ->
|
||||
remote.getCurrentWindow().isFullScreen()
|
||||
|
||||
setWindowFullScreen: (fullScreen=false) ->
|
||||
ipcRenderer.send("call-window-method", "setFullScreen", fullScreen)
|
||||
|
||||
openWindowDevTools: ->
|
||||
new Promise (resolve) ->
|
||||
# Defer DevTools interaction to the next tick, because using them during
|
||||
# event handling causes some wrong input events to be triggered on
|
||||
# `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697).
|
||||
process.nextTick ->
|
||||
if remote.getCurrentWindow().isDevToolsOpened()
|
||||
resolve()
|
||||
else
|
||||
remote.getCurrentWindow().once("devtools-opened", -> resolve())
|
||||
ipcRenderer.send("call-window-method", "openDevTools")
|
||||
|
||||
closeWindowDevTools: ->
|
||||
new Promise (resolve) ->
|
||||
# Defer DevTools interaction to the next tick, because using them during
|
||||
# event handling causes some wrong input events to be triggered on
|
||||
# `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697).
|
||||
process.nextTick ->
|
||||
unless remote.getCurrentWindow().isDevToolsOpened()
|
||||
resolve()
|
||||
else
|
||||
remote.getCurrentWindow().once("devtools-closed", -> resolve())
|
||||
ipcRenderer.send("call-window-method", "closeDevTools")
|
||||
|
||||
toggleWindowDevTools: ->
|
||||
new Promise (resolve) =>
|
||||
# Defer DevTools interaction to the next tick, because using them during
|
||||
# event handling causes some wrong input events to be triggered on
|
||||
# `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697).
|
||||
process.nextTick =>
|
||||
if remote.getCurrentWindow().isDevToolsOpened()
|
||||
@closeWindowDevTools().then(resolve)
|
||||
else
|
||||
@openWindowDevTools().then(resolve)
|
||||
|
||||
executeJavaScriptInWindowDevTools: (code) ->
|
||||
ipcRenderer.send("execute-javascript-in-dev-tools", code)
|
||||
|
||||
setWindowDocumentEdited: (edited) ->
|
||||
ipcRenderer.send("call-window-method", "setDocumentEdited", edited)
|
||||
|
||||
setRepresentedFilename: (filename) ->
|
||||
ipcRenderer.send("call-window-method", "setRepresentedFilename", filename)
|
||||
|
||||
addRecentDocument: (filename) ->
|
||||
ipcRenderer.send("add-recent-document", filename)
|
||||
|
||||
setRepresentedDirectoryPaths: (paths) ->
|
||||
loadSettings = getWindowLoadSettings()
|
||||
loadSettings['initialPaths'] = paths
|
||||
setWindowLoadSettings(loadSettings)
|
||||
|
||||
setAutoHideWindowMenuBar: (autoHide) ->
|
||||
ipcRenderer.send("call-window-method", "setAutoHideMenuBar", autoHide)
|
||||
|
||||
setWindowMenuBarVisibility: (visible) ->
|
||||
remote.getCurrentWindow().setMenuBarVisibility(visible)
|
||||
|
||||
getPrimaryDisplayWorkAreaSize: ->
|
||||
remote.screen.getPrimaryDisplay().workAreaSize
|
||||
|
||||
confirm: ({message, detailedMessage, buttons}) ->
|
||||
buttons ?= {}
|
||||
if _.isArray(buttons)
|
||||
buttonLabels = buttons
|
||||
else
|
||||
buttonLabels = Object.keys(buttons)
|
||||
|
||||
chosen = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'info'
|
||||
message: message
|
||||
detail: detailedMessage
|
||||
buttons: buttonLabels
|
||||
})
|
||||
|
||||
if _.isArray(buttons)
|
||||
chosen
|
||||
else
|
||||
callback = buttons[buttonLabels[chosen]]
|
||||
callback?()
|
||||
|
||||
showMessageDialog: (params) ->
|
||||
|
||||
showSaveDialog: (params) ->
|
||||
if _.isString(params)
|
||||
params = defaultPath: params
|
||||
else
|
||||
params = _.clone(params)
|
||||
params.title ?= 'Save File'
|
||||
params.defaultPath ?= getWindowLoadSettings().initialPaths[0]
|
||||
remote.dialog.showSaveDialog remote.getCurrentWindow(), params
|
||||
|
||||
playBeepSound: ->
|
||||
shell.beep()
|
||||
|
||||
onDidOpenLocations: (callback) ->
|
||||
outerCallback = (event, message, detail) ->
|
||||
callback(detail) if message is 'open-locations'
|
||||
|
||||
ipcRenderer.on('message', outerCallback)
|
||||
new Disposable ->
|
||||
ipcRenderer.removeListener('message', outerCallback)
|
||||
|
||||
onUpdateAvailable: (callback) ->
|
||||
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'
|
||||
|
||||
ipcRenderer.on('message', outerCallback)
|
||||
new Disposable ->
|
||||
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) ->
|
||||
outerCallback = (event, args...) ->
|
||||
callback(args...)
|
||||
|
||||
ipcRenderer.on('command', outerCallback)
|
||||
new Disposable ->
|
||||
ipcRenderer.removeListener('command', outerCallback)
|
||||
|
||||
onContextMenuCommand: (callback) ->
|
||||
outerCallback = (event, args...) ->
|
||||
callback(args...)
|
||||
|
||||
ipcRenderer.on('context-command', outerCallback)
|
||||
new Disposable ->
|
||||
ipcRenderer.removeListener('context-command', outerCallback)
|
||||
|
||||
didCancelWindowUnload: ->
|
||||
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')
|
||||
377
src/application-delegate.js
Normal file
377
src/application-delegate.js
Normal file
@@ -0,0 +1,377 @@
|
||||
const {ipcRenderer, remote, shell} = require('electron')
|
||||
const ipcHelpers = require('./ipc-helpers')
|
||||
const {Emitter, Disposable} = require('event-kit')
|
||||
const getWindowLoadSettings = require('./get-window-load-settings')
|
||||
|
||||
module.exports =
|
||||
class ApplicationDelegate {
|
||||
constructor () {
|
||||
this.pendingSettingsUpdateCount = 0
|
||||
this._ipcMessageEmitter = null
|
||||
}
|
||||
|
||||
ipcMessageEmitter () {
|
||||
if (!this._ipcMessageEmitter) {
|
||||
this._ipcMessageEmitter = new Emitter()
|
||||
ipcRenderer.on('message', (event, message, detail) => {
|
||||
this._ipcMessageEmitter.emit(message, detail)
|
||||
})
|
||||
}
|
||||
return this._ipcMessageEmitter
|
||||
}
|
||||
|
||||
getWindowLoadSettings () { return getWindowLoadSettings() }
|
||||
|
||||
open (params) {
|
||||
return ipcRenderer.send('open', params)
|
||||
}
|
||||
|
||||
pickFolder (callback) {
|
||||
const responseChannel = 'atom-pick-folder-response'
|
||||
ipcRenderer.on(responseChannel, function (event, path) {
|
||||
ipcRenderer.removeAllListeners(responseChannel)
|
||||
return callback(path)
|
||||
})
|
||||
return ipcRenderer.send('pick-folder', responseChannel)
|
||||
}
|
||||
|
||||
getCurrentWindow () {
|
||||
return remote.getCurrentWindow()
|
||||
}
|
||||
|
||||
closeWindow () {
|
||||
return ipcHelpers.call('window-method', 'close')
|
||||
}
|
||||
|
||||
async getTemporaryWindowState () {
|
||||
const stateJSON = await ipcHelpers.call('get-temporary-window-state')
|
||||
return JSON.parse(stateJSON)
|
||||
}
|
||||
|
||||
setTemporaryWindowState (state) {
|
||||
return ipcHelpers.call('set-temporary-window-state', JSON.stringify(state))
|
||||
}
|
||||
|
||||
getWindowSize () {
|
||||
const [width, height] = Array.from(remote.getCurrentWindow().getSize())
|
||||
return {width, height}
|
||||
}
|
||||
|
||||
setWindowSize (width, height) {
|
||||
return ipcHelpers.call('set-window-size', width, height)
|
||||
}
|
||||
|
||||
getWindowPosition () {
|
||||
const [x, y] = Array.from(remote.getCurrentWindow().getPosition())
|
||||
return {x, y}
|
||||
}
|
||||
|
||||
setWindowPosition (x, y) {
|
||||
return ipcHelpers.call('set-window-position', x, y)
|
||||
}
|
||||
|
||||
centerWindow () {
|
||||
return ipcHelpers.call('center-window')
|
||||
}
|
||||
|
||||
focusWindow () {
|
||||
return ipcHelpers.call('focus-window')
|
||||
}
|
||||
|
||||
showWindow () {
|
||||
return ipcHelpers.call('show-window')
|
||||
}
|
||||
|
||||
hideWindow () {
|
||||
return ipcHelpers.call('hide-window')
|
||||
}
|
||||
|
||||
reloadWindow () {
|
||||
return ipcHelpers.call('window-method', 'reload')
|
||||
}
|
||||
|
||||
restartApplication () {
|
||||
return ipcRenderer.send('restart-application')
|
||||
}
|
||||
|
||||
minimizeWindow () {
|
||||
return ipcHelpers.call('window-method', 'minimize')
|
||||
}
|
||||
|
||||
isWindowMaximized () {
|
||||
return remote.getCurrentWindow().isMaximized()
|
||||
}
|
||||
|
||||
maximizeWindow () {
|
||||
return ipcHelpers.call('window-method', 'maximize')
|
||||
}
|
||||
|
||||
unmaximizeWindow () {
|
||||
return ipcHelpers.call('window-method', 'unmaximize')
|
||||
}
|
||||
|
||||
isWindowFullScreen () {
|
||||
return remote.getCurrentWindow().isFullScreen()
|
||||
}
|
||||
|
||||
setWindowFullScreen (fullScreen = false) {
|
||||
return ipcHelpers.call('window-method', 'setFullScreen', fullScreen)
|
||||
}
|
||||
|
||||
onDidEnterFullScreen (callback) {
|
||||
return ipcHelpers.on(ipcRenderer, 'did-enter-full-screen', callback)
|
||||
}
|
||||
|
||||
onDidLeaveFullScreen (callback) {
|
||||
return ipcHelpers.on(ipcRenderer, 'did-leave-full-screen', callback)
|
||||
}
|
||||
|
||||
async openWindowDevTools () {
|
||||
// Defer DevTools interaction to the next tick, because using them during
|
||||
// event handling causes some wrong input events to be triggered on
|
||||
// `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697).
|
||||
await new Promise(process.nextTick)
|
||||
return ipcHelpers.call('window-method', 'openDevTools')
|
||||
}
|
||||
|
||||
async closeWindowDevTools () {
|
||||
// Defer DevTools interaction to the next tick, because using them during
|
||||
// event handling causes some wrong input events to be triggered on
|
||||
// `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697).
|
||||
await new Promise(process.nextTick)
|
||||
return ipcHelpers.call('window-method', 'closeDevTools')
|
||||
}
|
||||
|
||||
async toggleWindowDevTools () {
|
||||
// Defer DevTools interaction to the next tick, because using them during
|
||||
// event handling causes some wrong input events to be triggered on
|
||||
// `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697).
|
||||
await new Promise(process.nextTick)
|
||||
return ipcHelpers.call('window-method', 'toggleDevTools')
|
||||
}
|
||||
|
||||
executeJavaScriptInWindowDevTools (code) {
|
||||
return ipcRenderer.send('execute-javascript-in-dev-tools', code)
|
||||
}
|
||||
|
||||
didClosePathWithWaitSession (path) {
|
||||
return ipcHelpers.call('window-method', 'didClosePathWithWaitSession', path)
|
||||
}
|
||||
|
||||
setWindowDocumentEdited (edited) {
|
||||
return ipcHelpers.call('window-method', 'setDocumentEdited', edited)
|
||||
}
|
||||
|
||||
setRepresentedFilename (filename) {
|
||||
return ipcHelpers.call('window-method', 'setRepresentedFilename', filename)
|
||||
}
|
||||
|
||||
addRecentDocument (filename) {
|
||||
return ipcRenderer.send('add-recent-document', filename)
|
||||
}
|
||||
|
||||
setRepresentedDirectoryPaths (paths) {
|
||||
return ipcHelpers.call('window-method', 'setRepresentedDirectoryPaths', paths)
|
||||
}
|
||||
|
||||
setAutoHideWindowMenuBar (autoHide) {
|
||||
return ipcHelpers.call('window-method', 'setAutoHideMenuBar', autoHide)
|
||||
}
|
||||
|
||||
setWindowMenuBarVisibility (visible) {
|
||||
return remote.getCurrentWindow().setMenuBarVisibility(visible)
|
||||
}
|
||||
|
||||
getPrimaryDisplayWorkAreaSize () {
|
||||
return remote.screen.getPrimaryDisplay().workAreaSize
|
||||
}
|
||||
|
||||
getUserDefault (key, type) {
|
||||
return remote.systemPreferences.getUserDefault(key, type)
|
||||
}
|
||||
|
||||
async setUserSettings (config, configFilePath) {
|
||||
this.pendingSettingsUpdateCount++
|
||||
try {
|
||||
await ipcHelpers.call('set-user-settings', JSON.stringify(config), configFilePath)
|
||||
} finally {
|
||||
this.pendingSettingsUpdateCount--
|
||||
}
|
||||
}
|
||||
|
||||
onDidChangeUserSettings (callback) {
|
||||
return this.ipcMessageEmitter().on('did-change-user-settings', detail => {
|
||||
if (this.pendingSettingsUpdateCount === 0) callback(detail)
|
||||
})
|
||||
}
|
||||
|
||||
onDidFailToReadUserSettings (callback) {
|
||||
return this.ipcMessageEmitter().on('did-fail-to-read-user-setting', callback)
|
||||
}
|
||||
|
||||
confirm (options, callback) {
|
||||
if (typeof callback === 'function') {
|
||||
// Async version: pass options directly to Electron but set sane defaults
|
||||
options = Object.assign({type: 'info', normalizeAccessKeys: true}, options)
|
||||
remote.dialog.showMessageBox(remote.getCurrentWindow(), options, callback)
|
||||
} else {
|
||||
// Legacy sync version: options can only have `message`,
|
||||
// `detailedMessage` (optional), and buttons array or object (optional)
|
||||
let {message, detailedMessage, buttons} = options
|
||||
|
||||
let buttonLabels
|
||||
if (!buttons) buttons = {}
|
||||
if (Array.isArray(buttons)) {
|
||||
buttonLabels = buttons
|
||||
} else {
|
||||
buttonLabels = Object.keys(buttons)
|
||||
}
|
||||
|
||||
const chosen = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'info',
|
||||
message,
|
||||
detail: detailedMessage,
|
||||
buttons: buttonLabels,
|
||||
normalizeAccessKeys: true
|
||||
})
|
||||
|
||||
if (Array.isArray(buttons)) {
|
||||
return chosen
|
||||
} else {
|
||||
const callback = buttons[buttonLabels[chosen]]
|
||||
if (typeof callback === 'function') return callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showMessageDialog (params) {}
|
||||
|
||||
showSaveDialog (options, callback) {
|
||||
if (typeof callback === 'function') {
|
||||
// Async
|
||||
this.getCurrentWindow().showSaveDialog(options, callback)
|
||||
} else {
|
||||
// Sync
|
||||
if (typeof options === 'string') {
|
||||
options = {defaultPath: options}
|
||||
}
|
||||
return this.getCurrentWindow().showSaveDialog(options)
|
||||
}
|
||||
}
|
||||
|
||||
playBeepSound () {
|
||||
return shell.beep()
|
||||
}
|
||||
|
||||
onDidOpenLocations (callback) {
|
||||
return this.ipcMessageEmitter().on('open-locations', callback)
|
||||
}
|
||||
|
||||
onUpdateAvailable (callback) {
|
||||
// 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.
|
||||
return this.ipcMessageEmitter().on('did-begin-downloading-update', callback)
|
||||
}
|
||||
|
||||
onDidBeginDownloadingUpdate (callback) {
|
||||
return this.onUpdateAvailable(callback)
|
||||
}
|
||||
|
||||
onDidBeginCheckingForUpdate (callback) {
|
||||
return this.ipcMessageEmitter().on('checking-for-update', callback)
|
||||
}
|
||||
|
||||
onDidCompleteDownloadingUpdate (callback) {
|
||||
return this.ipcMessageEmitter().on('update-available', callback)
|
||||
}
|
||||
|
||||
onUpdateNotAvailable (callback) {
|
||||
return this.ipcMessageEmitter().on('update-not-available', callback)
|
||||
}
|
||||
|
||||
onUpdateError (callback) {
|
||||
return this.ipcMessageEmitter().on('update-error', callback)
|
||||
}
|
||||
|
||||
onApplicationMenuCommand (handler) {
|
||||
const outerCallback = (event, ...args) => handler(...args)
|
||||
|
||||
ipcRenderer.on('command', outerCallback)
|
||||
return new Disposable(() => ipcRenderer.removeListener('command', outerCallback))
|
||||
}
|
||||
|
||||
onContextMenuCommand (handler) {
|
||||
const outerCallback = (event, ...args) => handler(...args)
|
||||
|
||||
ipcRenderer.on('context-command', outerCallback)
|
||||
return new Disposable(() => ipcRenderer.removeListener('context-command', outerCallback))
|
||||
}
|
||||
|
||||
onURIMessage (handler) {
|
||||
const outerCallback = (event, ...args) => handler(...args)
|
||||
|
||||
ipcRenderer.on('uri-message', outerCallback)
|
||||
return new Disposable(() => ipcRenderer.removeListener('uri-message', outerCallback))
|
||||
}
|
||||
|
||||
onDidRequestUnload (callback) {
|
||||
const outerCallback = async (event, message) => {
|
||||
const shouldUnload = await callback(event)
|
||||
ipcRenderer.send('did-prepare-to-unload', shouldUnload)
|
||||
}
|
||||
|
||||
ipcRenderer.on('prepare-to-unload', outerCallback)
|
||||
return new Disposable(() => ipcRenderer.removeListener('prepare-to-unload', outerCallback))
|
||||
}
|
||||
|
||||
onDidChangeHistoryManager (callback) {
|
||||
const outerCallback = (event, message) => callback(event)
|
||||
|
||||
ipcRenderer.on('did-change-history-manager', outerCallback)
|
||||
return new Disposable(() => ipcRenderer.removeListener('did-change-history-manager', outerCallback))
|
||||
}
|
||||
|
||||
didChangeHistoryManager () {
|
||||
return ipcRenderer.send('did-change-history-manager')
|
||||
}
|
||||
|
||||
openExternal (url) {
|
||||
return shell.openExternal(url)
|
||||
}
|
||||
|
||||
checkForUpdate () {
|
||||
return ipcRenderer.send('command', 'application:check-for-update')
|
||||
}
|
||||
|
||||
restartAndInstallUpdate () {
|
||||
return ipcRenderer.send('command', 'application:install-update')
|
||||
}
|
||||
|
||||
getAutoUpdateManagerState () {
|
||||
return ipcRenderer.sendSync('get-auto-update-manager-state')
|
||||
}
|
||||
|
||||
getAutoUpdateManagerErrorMessage () {
|
||||
return ipcRenderer.sendSync('get-auto-update-manager-error')
|
||||
}
|
||||
|
||||
emitWillSavePath (path) {
|
||||
return ipcHelpers.call('will-save-path', path)
|
||||
}
|
||||
|
||||
emitDidSavePath (path) {
|
||||
return ipcHelpers.call('did-save-path', path)
|
||||
}
|
||||
|
||||
resolveProxy (requestId, url) {
|
||||
return ipcRenderer.send('resolve-proxy', requestId, url)
|
||||
}
|
||||
|
||||
onDidResolveProxy (callback) {
|
||||
const outerCallback = (event, requestId, proxy) => callback(requestId, proxy)
|
||||
|
||||
ipcRenderer.on('did-resolve-proxy', outerCallback)
|
||||
return new Disposable(() => ipcRenderer.removeListener('did-resolve-proxy', outerCallback))
|
||||
}
|
||||
}
|
||||
@@ -1,951 +0,0 @@
|
||||
crypto = require 'crypto'
|
||||
path = require 'path'
|
||||
{ipcRenderer} = require 'electron'
|
||||
|
||||
_ = require 'underscore-plus'
|
||||
{deprecate} = require 'grim'
|
||||
environmentHelpers = require('./environment-helpers')
|
||||
{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'
|
||||
StateStore = require './state-store'
|
||||
{getWindowLoadSettings, setWindowLoadSettings} = require './window-load-settings-helpers'
|
||||
registerDefaultCommands = require './register-default-commands'
|
||||
|
||||
DeserializerManager = require './deserializer-manager'
|
||||
ViewRegistry = require './view-registry'
|
||||
NotificationManager = require './notification-manager'
|
||||
Config = require './config'
|
||||
KeymapManager = require './keymap-extensions'
|
||||
TooltipManager = require './tooltip-manager'
|
||||
CommandRegistry = require './command-registry'
|
||||
GrammarRegistry = require './grammar-registry'
|
||||
StyleManager = require './style-manager'
|
||||
PackageManager = require './package-manager'
|
||||
ThemeManager = require './theme-manager'
|
||||
MenuManager = require './menu-manager'
|
||||
ContextMenuManager = require './context-menu-manager'
|
||||
CommandInstaller = require './command-installer'
|
||||
Clipboard = require './clipboard'
|
||||
Project = require './project'
|
||||
Workspace = require './workspace'
|
||||
PanelContainer = require './panel-container'
|
||||
Panel = require './panel'
|
||||
PaneContainer = require './pane-container'
|
||||
PaneAxis = require './pane-axis'
|
||||
Pane = require './pane'
|
||||
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'
|
||||
PanelElement = require './panel-element'
|
||||
PaneContainerElement = require './pane-container-element'
|
||||
PaneAxisElement = require './pane-axis-element'
|
||||
PaneElement = require './pane-element'
|
||||
TextEditorElement = require './text-editor-element'
|
||||
{createGutterView} = require './gutter-component-helpers'
|
||||
|
||||
# Essential: Atom global for dealing with packages, themes, menus, and the window.
|
||||
#
|
||||
# An instance of this class is always available as the `atom` global.
|
||||
module.exports =
|
||||
class AtomEnvironment extends Model
|
||||
@version: 1 # Increment this when the serialization format changes
|
||||
|
||||
lastUncaughtError: null
|
||||
|
||||
###
|
||||
Section: Properties
|
||||
###
|
||||
|
||||
# Public: A {CommandRegistry} instance
|
||||
commands: null
|
||||
|
||||
# Public: A {Config} instance
|
||||
config: null
|
||||
|
||||
# Public: A {Clipboard} instance
|
||||
clipboard: null
|
||||
|
||||
# Public: A {ContextMenuManager} instance
|
||||
contextMenu: null
|
||||
|
||||
# Public: A {MenuManager} instance
|
||||
menu: null
|
||||
|
||||
# Public: A {KeymapManager} instance
|
||||
keymaps: null
|
||||
|
||||
# Public: A {TooltipManager} instance
|
||||
tooltips: null
|
||||
|
||||
# Public: A {NotificationManager} instance
|
||||
notifications: null
|
||||
|
||||
# Public: A {Project} instance
|
||||
project: null
|
||||
|
||||
# Public: A {GrammarRegistry} instance
|
||||
grammars: null
|
||||
|
||||
# Public: A {PackageManager} instance
|
||||
packages: null
|
||||
|
||||
# Public: A {ThemeManager} instance
|
||||
themes: null
|
||||
|
||||
# Public: A {StyleManager} instance
|
||||
styles: null
|
||||
|
||||
# Public: A {DeserializerManager} instance
|
||||
deserializers: null
|
||||
|
||||
# Public: A {ViewRegistry} instance
|
||||
views: null
|
||||
|
||||
# Public: A {Workspace} instance
|
||||
workspace: null
|
||||
|
||||
# Public: A {TextEditorRegistry} instance
|
||||
textEditors: null
|
||||
|
||||
# Private: An {AutoUpdateManager} instance
|
||||
autoUpdater: null
|
||||
|
||||
saveStateDebounceInterval: 1000
|
||||
|
||||
###
|
||||
Section: Construction and Destruction
|
||||
###
|
||||
|
||||
# Call .loadOrCreate instead
|
||||
constructor: (params={}) ->
|
||||
environmentHelpers.normalize(params)
|
||||
{@blobStore, @applicationDelegate, @window, @document, configDirPath, @enablePersistence, onlyLoadBaseStyleSheets} = params
|
||||
|
||||
@unloaded = false
|
||||
@loadTime = null
|
||||
{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 = {}
|
||||
|
||||
@views = new ViewRegistry(this)
|
||||
|
||||
@notifications = new NotificationManager
|
||||
|
||||
@config = new Config({configDirPath, resourcePath, notificationManager: @notifications, @enablePersistence})
|
||||
@setConfigSchema()
|
||||
|
||||
@keymaps = new KeymapManager({configDirPath, resourcePath, notificationManager: @notifications})
|
||||
|
||||
@tooltips = new TooltipManager(keymapManager: @keymaps)
|
||||
|
||||
@commands = new CommandRegistry
|
||||
@commands.attach(@window)
|
||||
|
||||
@grammars = new GrammarRegistry({@config})
|
||||
|
||||
@styles = new StyleManager({configDirPath})
|
||||
|
||||
@packages = new PackageManager({
|
||||
devMode, configDirPath, resourcePath, safeMode, @config, styleManager: @styles,
|
||||
commandRegistry: @commands, keymapManager: @keymaps, notificationManager: @notifications,
|
||||
grammarRegistry: @grammars, deserializerManager: @deserializers, viewRegistry: @views
|
||||
})
|
||||
|
||||
@themes = new ThemeManager({
|
||||
packageManager: @packages, configDirPath, resourcePath, safeMode, @config,
|
||||
styleManager: @styles, notificationManager: @notifications, viewRegistry: @views
|
||||
})
|
||||
|
||||
@menu = new MenuManager({resourcePath, keymapManager: @keymaps, packageManager: @packages})
|
||||
|
||||
@contextMenu = new ContextMenuManager({resourcePath, devMode, keymapManager: @keymaps})
|
||||
|
||||
@packages.setMenuManager(@menu)
|
||||
@packages.setContextMenuManager(@contextMenu)
|
||||
@packages.setThemeManager(@themes)
|
||||
|
||||
@clipboard = new Clipboard()
|
||||
|
||||
@project = new Project({notificationManager: @notifications, packageManager: @packages, @config})
|
||||
|
||||
@commandInstaller = new CommandInstaller(@getVersion(), @applicationDelegate)
|
||||
|
||||
@workspace = new Workspace({
|
||||
@config, @project, packageManager: @packages, grammarRegistry: @grammars, deserializerManager: @deserializers,
|
||||
notificationManager: @notifications, @applicationDelegate, @clipboard, viewRegistry: @views, assert: @assert.bind(this)
|
||||
})
|
||||
@themes.workspace = @workspace
|
||||
|
||||
@textEditors = new TextEditorRegistry
|
||||
@autoUpdater = new AutoUpdateManager({@applicationDelegate})
|
||||
|
||||
@config.load()
|
||||
|
||||
@themes.loadBaseStylesheets()
|
||||
@initialStyleElements = @styles.getSnapshot()
|
||||
@themes.initialLoadComplete = true if onlyLoadBaseStyleSheets
|
||||
@setBodyPlatformClass()
|
||||
|
||||
@stylesElement = @styles.buildStylesElement()
|
||||
@document.head.appendChild(@stylesElement)
|
||||
|
||||
@applicationDelegate.disablePinchToZoom()
|
||||
|
||||
@keymaps.subscribeToFileReadFailure()
|
||||
@keymaps.loadBundledKeymaps()
|
||||
|
||||
@registerDefaultCommands()
|
||||
@registerDefaultOpeners()
|
||||
@registerDefaultDeserializers()
|
||||
@registerDefaultViewProviders()
|
||||
|
||||
@installUncaughtErrorHandler()
|
||||
@attachSaveStateListeners()
|
||||
@installWindowEventHandler()
|
||||
|
||||
@observeAutoHideMenuBar()
|
||||
|
||||
checkPortableHomeWritable = ->
|
||||
responseChannel = "check-portable-home-writable-response"
|
||||
ipcRenderer.on responseChannel, (event, response) ->
|
||||
ipcRenderer.removeAllListeners(responseChannel)
|
||||
atom.notifications.addWarning("#{response.message.replace(/([\\\.+\\-_#!])/g, '\\$1')}") if not response.writable
|
||||
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'))}
|
||||
|
||||
registerDefaultDeserializers: ->
|
||||
@deserializers.add(Workspace)
|
||||
@deserializers.add(PaneContainer)
|
||||
@deserializers.add(PaneAxis)
|
||||
@deserializers.add(Pane)
|
||||
@deserializers.add(Project)
|
||||
@deserializers.add(TextEditor)
|
||||
@deserializers.add(TextBuffer)
|
||||
|
||||
registerDefaultCommands: ->
|
||||
registerDefaultCommands({commandRegistry: @commands, @config, @commandInstaller})
|
||||
|
||||
registerDefaultViewProviders: ->
|
||||
@views.addViewProvider Workspace, (model, env) ->
|
||||
new WorkspaceElement().initialize(model, env)
|
||||
@views.addViewProvider PanelContainer, (model, env) ->
|
||||
new PanelContainerElement().initialize(model, env)
|
||||
@views.addViewProvider Panel, (model, env) ->
|
||||
new PanelElement().initialize(model, env)
|
||||
@views.addViewProvider PaneContainer, (model, env) ->
|
||||
new PaneContainerElement().initialize(model, env)
|
||||
@views.addViewProvider PaneAxis, (model, env) ->
|
||||
new PaneAxisElement().initialize(model, env)
|
||||
@views.addViewProvider Pane, (model, env) ->
|
||||
new PaneElement().initialize(model, env)
|
||||
@views.addViewProvider(Gutter, createGutterView)
|
||||
|
||||
registerDefaultOpeners: ->
|
||||
@workspace.addOpener (uri) =>
|
||||
switch uri
|
||||
when 'atom://.atom/stylesheet'
|
||||
@workspace.open(@styles.getUserStyleSheetPath())
|
||||
when 'atom://.atom/keymap'
|
||||
@workspace.open(@keymaps.getUserKeymapPath())
|
||||
when 'atom://.atom/config'
|
||||
@workspace.open(@config.getUserConfigPath())
|
||||
when 'atom://.atom/init-script'
|
||||
@workspace.open(@getUserInitScriptPath())
|
||||
|
||||
registerDefaultTargetForKeymaps: ->
|
||||
@keymaps.defaultTarget = @views.getView(@workspace)
|
||||
|
||||
observeAutoHideMenuBar: ->
|
||||
@disposables.add @config.onDidChange 'core.autoHideMenuBar', ({newValue}) =>
|
||||
@setAutoHideMenuBar(newValue)
|
||||
@setAutoHideMenuBar(true) if @config.get('core.autoHideMenuBar')
|
||||
|
||||
reset: ->
|
||||
@deserializers.clear()
|
||||
@registerDefaultDeserializers()
|
||||
|
||||
@config.clear()
|
||||
@setConfigSchema()
|
||||
|
||||
@keymaps.clear()
|
||||
@keymaps.loadBundledKeymaps()
|
||||
|
||||
@commands.clear()
|
||||
@registerDefaultCommands()
|
||||
|
||||
@styles.restoreSnapshot(@initialStyleElements)
|
||||
|
||||
@menu.clear()
|
||||
|
||||
@clipboard.reset()
|
||||
|
||||
@notifications.clear()
|
||||
|
||||
@contextMenu.clear()
|
||||
|
||||
@packages.reset()
|
||||
|
||||
@workspace.reset(@packages)
|
||||
@registerDefaultOpeners()
|
||||
|
||||
@project.reset(@packages)
|
||||
|
||||
@workspace.subscribeToEvents()
|
||||
|
||||
@grammars.clear()
|
||||
|
||||
@views.clear()
|
||||
@registerDefaultViewProviders()
|
||||
|
||||
destroy: ->
|
||||
return if not @project
|
||||
|
||||
@disposables.dispose()
|
||||
@workspace?.destroy()
|
||||
@workspace = null
|
||||
@themes.workspace = null
|
||||
@project?.destroy()
|
||||
@project = null
|
||||
@commands.clear()
|
||||
@stylesElement.remove()
|
||||
@config.unobserveUserConfig()
|
||||
@autoUpdater.destroy()
|
||||
|
||||
@uninstallWindowEventHandler()
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
###
|
||||
|
||||
# Extended: Invoke the given callback whenever {::beep} is called.
|
||||
#
|
||||
# * `callback` {Function} to be called whenever {::beep} is called.
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidBeep: (callback) ->
|
||||
@emitter.on 'did-beep', callback
|
||||
|
||||
# Extended: Invoke the given callback when there is an unhandled error, but
|
||||
# before the devtools pop open
|
||||
#
|
||||
# * `callback` {Function} to be called whenever there is an unhandled error
|
||||
# * `event` {Object}
|
||||
# * `originalError` {Object} the original error object
|
||||
# * `message` {String} the original error object
|
||||
# * `url` {String} Url to the file where the error originated.
|
||||
# * `line` {Number}
|
||||
# * `column` {Number}
|
||||
# * `preventDefault` {Function} call this to avoid popping up the dev tools.
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onWillThrowError: (callback) ->
|
||||
@emitter.on 'will-throw-error', callback
|
||||
|
||||
# Extended: Invoke the given callback whenever there is an unhandled error.
|
||||
#
|
||||
# * `callback` {Function} to be called whenever there is an unhandled error
|
||||
# * `event` {Object}
|
||||
# * `originalError` {Object} the original error object
|
||||
# * `message` {String} the original error object
|
||||
# * `url` {String} Url to the file where the error originated.
|
||||
# * `line` {Number}
|
||||
# * `column` {Number}
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidThrowError: (callback) ->
|
||||
@emitter.on 'did-throw-error', callback
|
||||
|
||||
# TODO: Make this part of the public API. We should make onDidThrowError
|
||||
# match the interface by only yielding an exception object to the handler
|
||||
# and deprecating the old behavior.
|
||||
onDidFailAssertion: (callback) ->
|
||||
@emitter.on 'did-fail-assertion', callback
|
||||
|
||||
###
|
||||
Section: Atom Details
|
||||
###
|
||||
|
||||
# Public: Returns a {Boolean} that is `true` if the current window is in development mode.
|
||||
inDevMode: ->
|
||||
@devMode ?= @getLoadSettings().devMode
|
||||
|
||||
# Public: Returns a {Boolean} that is `true` if the current window is in safe mode.
|
||||
inSafeMode: ->
|
||||
@safeMode ?= @getLoadSettings().safeMode
|
||||
|
||||
# Public: Returns a {Boolean} that is `true` if the current window is running specs.
|
||||
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
|
||||
|
||||
# Public: Get the time taken to completely load the current window.
|
||||
#
|
||||
# This time include things like loading and activating packages, creating
|
||||
# DOM elements for the editor, and reading the config.
|
||||
#
|
||||
# Returns the {Number} of milliseconds taken to load the window or null
|
||||
# if the window hasn't finished loading yet.
|
||||
getWindowLoadTime: ->
|
||||
@loadTime
|
||||
|
||||
# Public: Get the load settings for the current window.
|
||||
#
|
||||
# Returns an {Object} containing all the load setting key/value pairs.
|
||||
getLoadSettings: ->
|
||||
getWindowLoadSettings()
|
||||
|
||||
###
|
||||
Section: Managing The Atom Window
|
||||
###
|
||||
|
||||
# Essential: Open a new Atom window using the given options.
|
||||
#
|
||||
# Calling this method without an options parameter will open a prompt to pick
|
||||
# a file/folder to open in the new window.
|
||||
#
|
||||
# * `params` An {Object} with the following keys:
|
||||
# * `pathsToOpen` An {Array} of {String} paths to open.
|
||||
# * `newWindow` A {Boolean}, true to always open a new window instead of
|
||||
# reusing existing windows depending on the paths to open.
|
||||
# * `devMode` A {Boolean}, true to open the window in development mode.
|
||||
# Development mode loads the Atom source from the locally cloned
|
||||
# repository and also loads all the packages in ~/.atom/dev/packages
|
||||
# * `safeMode` A {Boolean}, true to open the window in safe mode. Safe
|
||||
# mode prevents all packages installed to ~/.atom/packages from loading.
|
||||
open: (params) ->
|
||||
@applicationDelegate.open(params)
|
||||
|
||||
# Extended: Prompt the user to select one or more folders.
|
||||
#
|
||||
# * `callback` A {Function} to call once the user has confirmed the selection.
|
||||
# * `paths` An {Array} of {String} paths that the user selected, or `null`
|
||||
# if the user dismissed the dialog.
|
||||
pickFolder: (callback) ->
|
||||
@applicationDelegate.pickFolder(callback)
|
||||
|
||||
# Essential: Close the current window.
|
||||
close: ->
|
||||
@applicationDelegate.closeWindow()
|
||||
|
||||
# Essential: Get the size of current window.
|
||||
#
|
||||
# Returns an {Object} in the format `{width: 1000, height: 700}`
|
||||
getSize: ->
|
||||
@applicationDelegate.getWindowSize()
|
||||
|
||||
# Essential: Set the size of current window.
|
||||
#
|
||||
# * `width` The {Number} of pixels.
|
||||
# * `height` The {Number} of pixels.
|
||||
setSize: (width, height) ->
|
||||
@applicationDelegate.setWindowSize(width, height)
|
||||
|
||||
# Essential: Get the position of current window.
|
||||
#
|
||||
# Returns an {Object} in the format `{x: 10, y: 20}`
|
||||
getPosition: ->
|
||||
@applicationDelegate.getWindowPosition()
|
||||
|
||||
# Essential: Set the position of current window.
|
||||
#
|
||||
# * `x` The {Number} of pixels.
|
||||
# * `y` The {Number} of pixels.
|
||||
setPosition: (x, y) ->
|
||||
@applicationDelegate.setWindowPosition(x, y)
|
||||
|
||||
# Extended: Get the current window
|
||||
getCurrentWindow: ->
|
||||
@applicationDelegate.getCurrentWindow()
|
||||
|
||||
# Extended: Move current window to the center of the screen.
|
||||
center: ->
|
||||
@applicationDelegate.centerWindow()
|
||||
|
||||
# Extended: Focus the current window.
|
||||
focus: ->
|
||||
@applicationDelegate.focusWindow()
|
||||
@window.focus()
|
||||
|
||||
# Extended: Show the current window.
|
||||
show: ->
|
||||
@applicationDelegate.showWindow()
|
||||
|
||||
# Extended: Hide the current window.
|
||||
hide: ->
|
||||
@applicationDelegate.hideWindow()
|
||||
|
||||
# Extended: Reload the current window.
|
||||
reload: ->
|
||||
@applicationDelegate.reloadWindow()
|
||||
|
||||
# Extended: Returns a {Boolean} that is `true` if the current window is maximized.
|
||||
isMaximized: ->
|
||||
@applicationDelegate.isWindowMaximized()
|
||||
|
||||
maximize: ->
|
||||
@applicationDelegate.maximizeWindow()
|
||||
|
||||
# Extended: Returns a {Boolean} that is `true` if the current window is in full screen mode.
|
||||
isFullScreen: ->
|
||||
@applicationDelegate.isWindowFullScreen()
|
||||
|
||||
# Extended: Set the full screen state of the current window.
|
||||
setFullScreen: (fullScreen=false) ->
|
||||
@applicationDelegate.setWindowFullScreen(fullScreen)
|
||||
if fullScreen
|
||||
@document.body.classList.add("fullscreen")
|
||||
else
|
||||
@document.body.classList.remove("fullscreen")
|
||||
|
||||
# Extended: Toggle the full screen state of the current window.
|
||||
toggleFullScreen: ->
|
||||
@setFullScreen(not @isFullScreen())
|
||||
|
||||
# Restore the window to its previous dimensions and show it.
|
||||
#
|
||||
# Restores the full screen and maximized state after the window has resized to
|
||||
# prevent resize glitches.
|
||||
displayWindow: ->
|
||||
@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.
|
||||
#
|
||||
# Returns an {Object} with the following keys:
|
||||
# * `x` The window's x-position {Number}.
|
||||
# * `y` The window's y-position {Number}.
|
||||
# * `width` The window's width {Number}.
|
||||
# * `height` The window's height {Number}.
|
||||
getWindowDimensions: ->
|
||||
browserWindow = @getCurrentWindow()
|
||||
[x, y] = browserWindow.getPosition()
|
||||
[width, height] = browserWindow.getSize()
|
||||
maximized = browserWindow.isMaximized()
|
||||
{x, y, width, height, maximized}
|
||||
|
||||
# Set the dimensions of the window.
|
||||
#
|
||||
# The window will be centered if either the x or y coordinate is not set
|
||||
# in the dimensions parameter. If x or y are omitted the window will be
|
||||
# centered. If height or width are omitted only the position will be changed.
|
||||
#
|
||||
# * `dimensions` An {Object} with the following keys:
|
||||
# * `x` The new x coordinate.
|
||||
# * `y` The new y coordinate.
|
||||
# * `width` The new width.
|
||||
# * `height` The new height.
|
||||
setWindowDimensions: ({x, y, width, height}) ->
|
||||
steps = []
|
||||
if width? and height?
|
||||
steps.push(@setSize(width, height))
|
||||
if x? and y?
|
||||
steps.push(@setPosition(x, y))
|
||||
else
|
||||
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
|
||||
|
||||
storeWindowDimensions: ->
|
||||
@windowDimensions = @getWindowDimensions()
|
||||
if @isValidDimensions(@windowDimensions)
|
||||
localStorage.setItem("defaultWindowDimensions", JSON.stringify(@windowDimensions))
|
||||
|
||||
getDefaultWindowDimensions: ->
|
||||
{windowDimensions} = @getLoadSettings()
|
||||
return windowDimensions if windowDimensions?
|
||||
|
||||
dimensions = null
|
||||
try
|
||||
dimensions = JSON.parse(localStorage.getItem("defaultWindowDimensions"))
|
||||
catch error
|
||||
console.warn "Error parsing default window dimensions", error
|
||||
localStorage.removeItem("defaultWindowDimensions")
|
||||
|
||||
if @isValidDimensions(dimensions)
|
||||
dimensions
|
||||
else
|
||||
{width, height} = @applicationDelegate.getPrimaryDisplayWorkAreaSize()
|
||||
{x: 0, y: 0, width: Math.min(1024, width), height}
|
||||
|
||||
restoreWindowDimensions: ->
|
||||
unless @windowDimensions? and @isValidDimensions(@windowDimensions)
|
||||
@windowDimensions = @getDefaultWindowDimensions()
|
||||
@setWindowDimensions(@windowDimensions).then -> @windowDimensions
|
||||
|
||||
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()
|
||||
|
||||
workspaceElement = @views.getView(@workspace)
|
||||
backgroundColor = @window.getComputedStyle(workspaceElement)['background-color']
|
||||
@window.localStorage.setItem('atom:window-background-color', backgroundColor)
|
||||
|
||||
# Call this method when establishing a real application window.
|
||||
startEditorWindow: ->
|
||||
@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()
|
||||
|
||||
@registerDefaultTargetForKeymaps()
|
||||
|
||||
@packages.loadPackages()
|
||||
|
||||
startTime = Date.now()
|
||||
@deserialize(state) if state?
|
||||
@deserializeTimings.atom = Date.now() - startTime
|
||||
|
||||
@document.body.appendChild(@views.getView(@workspace))
|
||||
@backgroundStylesheet?.remove()
|
||||
|
||||
@watchProjectPaths()
|
||||
|
||||
@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()
|
||||
@packages.deactivatePackages()
|
||||
@saveBlobStoreSync()
|
||||
@unloaded = true
|
||||
|
||||
openInitialEmptyEditorIfNecessary: ->
|
||||
return unless @config.get('core.openEmptyEditorOnStart')
|
||||
if @getLoadSettings().initialPaths?.length is 0 and @workspace.getPaneItems().length is 0
|
||||
@workspace.open(null)
|
||||
|
||||
installUncaughtErrorHandler: ->
|
||||
@previousWindowErrorHandler = @window.onerror
|
||||
@window.onerror = =>
|
||||
@lastUncaughtError = Array::slice.call(arguments)
|
||||
[message, url, line, column, originalError] = @lastUncaughtError
|
||||
|
||||
{line, column} = mapSourcePosition({source: url, line, column})
|
||||
|
||||
eventObject = {message, url, line, column, originalError}
|
||||
|
||||
openDevTools = true
|
||||
eventObject.preventDefault = -> openDevTools = false
|
||||
|
||||
@emitter.emit 'will-throw-error', eventObject
|
||||
|
||||
if openDevTools
|
||||
@openDevTools().then => @executeJavaScriptInDevTools('DevToolsAPI.showConsole()')
|
||||
|
||||
@emitter.emit 'did-throw-error', {message, url, line, column, originalError}
|
||||
|
||||
uninstallUncaughtErrorHandler: ->
|
||||
@window.onerror = @previousWindowErrorHandler
|
||||
|
||||
installWindowEventHandler: ->
|
||||
@windowEventHandler = new WindowEventHandler({atomEnvironment: this, @applicationDelegate, @window, @document})
|
||||
|
||||
uninstallWindowEventHandler: ->
|
||||
@windowEventHandler?.unsubscribe()
|
||||
|
||||
###
|
||||
Section: Messaging the User
|
||||
###
|
||||
|
||||
# Essential: Visually and audibly trigger a beep.
|
||||
beep: ->
|
||||
@applicationDelegate.playBeepSound() if @config.get('core.audioBeep')
|
||||
@emitter.emit 'did-beep'
|
||||
|
||||
# Essential: A flexible way to open a dialog akin to an alert dialog.
|
||||
#
|
||||
# ## Examples
|
||||
#
|
||||
# ```coffee
|
||||
# atom.confirm
|
||||
# message: 'How you feeling?'
|
||||
# detailedMessage: 'Be honest.'
|
||||
# buttons:
|
||||
# Good: -> window.alert('good to hear')
|
||||
# Bad: -> window.alert('bummer')
|
||||
# ```
|
||||
#
|
||||
# * `options` An {Object} with the following keys:
|
||||
# * `message` The {String} message to display.
|
||||
# * `detailedMessage` (optional) The {String} detailed message to display.
|
||||
# * `buttons` (optional) Either an array of strings or an object where keys are
|
||||
# button names and the values are callbacks to invoke when clicked.
|
||||
#
|
||||
# Returns the chosen button index {Number} if the buttons option was an array.
|
||||
confirm: (params={}) ->
|
||||
@applicationDelegate.confirm(params)
|
||||
|
||||
###
|
||||
Section: Managing the Dev Tools
|
||||
###
|
||||
|
||||
# Extended: Open the dev tools for the current window.
|
||||
#
|
||||
# Returns a {Promise} that resolves when the DevTools have been opened.
|
||||
openDevTools: ->
|
||||
@applicationDelegate.openWindowDevTools()
|
||||
|
||||
# Extended: Toggle the visibility of the dev tools for the current window.
|
||||
#
|
||||
# Returns a {Promise} that resolves when the DevTools have been opened or
|
||||
# closed.
|
||||
toggleDevTools: ->
|
||||
@applicationDelegate.toggleWindowDevTools()
|
||||
|
||||
# Extended: Execute code in dev tools.
|
||||
executeJavaScriptInDevTools: (code) ->
|
||||
@applicationDelegate.executeJavaScriptInWindowDevTools(code)
|
||||
|
||||
###
|
||||
Section: Private
|
||||
###
|
||||
|
||||
assert: (condition, message, callback) ->
|
||||
return true if condition
|
||||
|
||||
error = new Error("Assertion failed: #{message}")
|
||||
Error.captureStackTrace(error, @assert)
|
||||
callback?(error)
|
||||
|
||||
@emitter.emit 'did-fail-assertion', error
|
||||
|
||||
false
|
||||
|
||||
loadThemes: ->
|
||||
@themes.load()
|
||||
|
||||
# Notify the browser project of the window's current project path
|
||||
watchProjectPaths: ->
|
||||
@disposables.add @project.onDidChangePaths =>
|
||||
@applicationDelegate.setRepresentedDirectoryPaths(@project.getPaths())
|
||||
|
||||
setDocumentEdited: (edited) ->
|
||||
@applicationDelegate.setWindowDocumentEdited?(edited)
|
||||
|
||||
setRepresentedFilename: (filename) ->
|
||||
@applicationDelegate.setWindowRepresentedFilename?(filename)
|
||||
|
||||
addProjectFolder: ->
|
||||
@pickFolder (selectedPaths = []) =>
|
||||
@project.addPath(selectedPath) for selectedPath in selectedPaths
|
||||
|
||||
showSaveDialog: (callback) ->
|
||||
callback(showSaveDialogSync())
|
||||
|
||||
showSaveDialogSync: (options={}) ->
|
||||
@applicationDelegate.showSaveDialog(options)
|
||||
|
||||
saveBlobStoreSync: ->
|
||||
return unless @enablePersistence
|
||||
|
||||
@blobStore.save()
|
||||
|
||||
saveState: (options) ->
|
||||
return Promise.resolve() unless @enablePersistence
|
||||
|
||||
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
|
||||
Promise.resolve(null)
|
||||
|
||||
deserialize: (state) ->
|
||||
if grammarOverridesByPath = state.grammars?.grammarOverridesByPath
|
||||
@grammars.grammarOverridesByPath = grammarOverridesByPath
|
||||
|
||||
@setFullScreen(state.fullScreen)
|
||||
|
||||
@packages.packageStates = state.packageStates ? {}
|
||||
|
||||
startTime = Date.now()
|
||||
@project.deserialize(state.project, @deserializers) if state.project?
|
||||
@deserializeTimings.project = Date.now() - startTime
|
||||
|
||||
startTime = Date.now()
|
||||
@workspace.deserialize(state.workspace, @deserializers) if state.workspace?
|
||||
@deserializeTimings.workspace = Date.now() - startTime
|
||||
|
||||
getStateKey: (paths) ->
|
||||
if paths?.length > 0
|
||||
sha1 = crypto.createHash('sha1').update(paths.slice().sort().join("\n")).digest('hex')
|
||||
"editor-#{sha1}"
|
||||
else
|
||||
null
|
||||
|
||||
getConfigDirPath: ->
|
||||
@configDirPath ?= process.env.ATOM_HOME
|
||||
|
||||
getUserInitScriptPath: ->
|
||||
initScriptPath = fs.resolve(@getConfigDirPath(), 'init', ['js', 'coffee'])
|
||||
initScriptPath ? path.join(@getConfigDirPath(), 'init.coffee')
|
||||
|
||||
requireUserInitScript: ->
|
||||
if userInitScriptPath = @getUserInitScriptPath()
|
||||
try
|
||||
require(userInitScriptPath) if fs.isFileSync(userInitScriptPath)
|
||||
catch error
|
||||
@notifications.addError "Failed to load `#{userInitScriptPath}`",
|
||||
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
|
||||
|
||||
updateAvailable: (details) ->
|
||||
@emitter.emit 'update-available', details
|
||||
|
||||
listenForUpdates: ->
|
||||
# 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}")
|
||||
|
||||
setAutoHideMenuBar: (autoHide) ->
|
||||
@applicationDelegate.setAutoHideWindowMenuBar(autoHide)
|
||||
@applicationDelegate.setWindowMenuBarVisibility(not autoHide)
|
||||
|
||||
dispatchApplicationMenuCommand: (command, arg) ->
|
||||
activeElement = @document.activeElement
|
||||
# Use the workspace element if body has focus
|
||||
if activeElement is @document.body and workspaceElement = @views.getView(@workspace)
|
||||
activeElement = workspaceElement
|
||||
@commands.dispatch(activeElement, command, arg)
|
||||
|
||||
dispatchContextMenuCommand: (command, args...) ->
|
||||
@commands.dispatch(@contextMenu.activeElement, command, args)
|
||||
|
||||
openLocations: (locations) ->
|
||||
needsProjectPaths = @project?.getPaths().length is 0
|
||||
|
||||
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))
|
||||
@project.addPath(path.dirname(pathToOpen))
|
||||
else
|
||||
@project.addPath(pathToOpen)
|
||||
|
||||
unless fs.isDirectorySync(pathToOpen)
|
||||
@workspace?.open(pathToOpen, {initialLine, initialColumn})
|
||||
|
||||
return
|
||||
|
||||
# Preserve this deprecation until 2.0. Sorry. Should have removed Q sooner.
|
||||
Promise.prototype.done = (callback) ->
|
||||
deprecate("Atom now uses ES6 Promises instead of Q. Call promise.then instead of promise.done")
|
||||
@then(callback)
|
||||
1443
src/atom-environment.js
Normal file
1443
src/atom-environment.js
Normal file
File diff suppressed because it is too large
Load Diff
60
src/atom-paths.js
Normal file
60
src/atom-paths.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const fs = require('fs-plus')
|
||||
const path = require('path')
|
||||
|
||||
const hasWriteAccess = (dir) => {
|
||||
const testFilePath = path.join(dir, 'write.test')
|
||||
try {
|
||||
fs.writeFileSync(testFilePath, new Date().toISOString(), { flag: 'w+' })
|
||||
fs.unlinkSync(testFilePath)
|
||||
return true
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const getAppDirectory = () => {
|
||||
switch (process.platform) {
|
||||
case 'darwin':
|
||||
return process.execPath.substring(0, process.execPath.indexOf('.app') + 4)
|
||||
case 'linux':
|
||||
case 'win32':
|
||||
return path.join(process.execPath, '..')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setAtomHome: (homePath) => {
|
||||
// When a read-writeable .atom folder exists above app use that
|
||||
const portableHomePath = path.join(getAppDirectory(), '..', '.atom')
|
||||
if (fs.existsSync(portableHomePath)) {
|
||||
if (hasWriteAccess(portableHomePath)) {
|
||||
process.env.ATOM_HOME = portableHomePath
|
||||
} else {
|
||||
// A path exists so it was intended to be used but we didn't have rights, so warn.
|
||||
console.log(`Insufficient permission to portable Atom home "${portableHomePath}".`)
|
||||
}
|
||||
}
|
||||
|
||||
// Check ATOM_HOME environment variable next
|
||||
if (process.env.ATOM_HOME !== undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
// Fall back to default .atom folder in users home folder
|
||||
process.env.ATOM_HOME = path.join(homePath, '.atom')
|
||||
},
|
||||
|
||||
setUserData: (app) => {
|
||||
const electronUserDataPath = path.join(process.env.ATOM_HOME, 'electronUserData')
|
||||
if (fs.existsSync(electronUserDataPath)) {
|
||||
if (hasWriteAccess(electronUserDataPath)) {
|
||||
app.setPath('userData', electronUserDataPath)
|
||||
} else {
|
||||
// A path exists so it was intended to be used but we didn't have rights, so warn.
|
||||
console.log(`Insufficient permission to Electron user data "${electronUserDataPath}".`)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getAppDirectory: getAppDirectory
|
||||
}
|
||||
@@ -1,25 +1,29 @@
|
||||
'use babel'
|
||||
const {Emitter, CompositeDisposable} = require('event-kit')
|
||||
|
||||
import {Emitter, CompositeDisposable} from 'event-kit'
|
||||
|
||||
export default class AutoUpdateManager {
|
||||
module.exports =
|
||||
class AutoUpdateManager {
|
||||
constructor ({applicationDelegate}) {
|
||||
this.applicationDelegate = applicationDelegate
|
||||
this.subscriptions = new CompositeDisposable()
|
||||
this.emitter = new Emitter()
|
||||
}
|
||||
|
||||
initialize () {
|
||||
this.subscriptions.add(
|
||||
applicationDelegate.onDidBeginCheckingForUpdate(() => {
|
||||
this.applicationDelegate.onDidBeginCheckingForUpdate(() => {
|
||||
this.emitter.emit('did-begin-checking-for-update')
|
||||
}),
|
||||
applicationDelegate.onDidBeginDownloadingUpdate(() => {
|
||||
this.applicationDelegate.onDidBeginDownloadingUpdate(() => {
|
||||
this.emitter.emit('did-begin-downloading-update')
|
||||
}),
|
||||
applicationDelegate.onDidCompleteDownloadingUpdate((details) => {
|
||||
this.applicationDelegate.onDidCompleteDownloadingUpdate((details) => {
|
||||
this.emitter.emit('did-complete-downloading-update', details)
|
||||
}),
|
||||
applicationDelegate.onUpdateNotAvailable(() => {
|
||||
this.applicationDelegate.onUpdateNotAvailable(() => {
|
||||
this.emitter.emit('update-not-available')
|
||||
}),
|
||||
this.applicationDelegate.onUpdateError(() => {
|
||||
this.emitter.emit('update-error')
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -41,6 +45,10 @@ export default class AutoUpdateManager {
|
||||
return this.applicationDelegate.getAutoUpdateManagerState()
|
||||
}
|
||||
|
||||
getErrorMessage () {
|
||||
return this.applicationDelegate.getAutoUpdateManagerErrorMessage()
|
||||
}
|
||||
|
||||
platformSupportsUpdates () {
|
||||
return atom.getReleaseChannel() !== 'dev' && this.getState() !== 'unsupported'
|
||||
}
|
||||
@@ -67,6 +75,10 @@ export default class AutoUpdateManager {
|
||||
return this.emitter.on('update-not-available', callback)
|
||||
}
|
||||
|
||||
onUpdateError (callback) {
|
||||
return this.emitter.on('update-error', callback)
|
||||
}
|
||||
|
||||
getPlatform () {
|
||||
return process.platform
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ var PREFIXES = [
|
||||
'/** @babel */',
|
||||
'"use babel"',
|
||||
'\'use babel\'',
|
||||
'/* @flow */'
|
||||
'/* @flow */',
|
||||
'// @flow'
|
||||
]
|
||||
|
||||
var PREFIX_LENGTH = Math.max.apply(Math, PREFIXES.map(function (prefix) {
|
||||
@@ -49,6 +50,10 @@ exports.compile = function (sourceCode, filePath) {
|
||||
Logger.prototype.verbose = noop
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
filePath = 'file:///' + path.resolve(filePath).replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
var options = {filename: filePath}
|
||||
for (var key in defaultOptions) {
|
||||
options[key] = defaultOptions[key]
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
cloneObject = (object) ->
|
||||
clone = {}
|
||||
clone[key] = value for key, value of object
|
||||
clone
|
||||
|
||||
module.exports =
|
||||
class BlockDecorationsComponent
|
||||
constructor: (@container, @views, @presenter, @domElementPool) ->
|
||||
@newState = null
|
||||
@oldState = null
|
||||
@blockDecorationNodesById = {}
|
||||
@domNode = @domElementPool.buildElement("content")
|
||||
@domNode.setAttribute("select", ".atom--invisible-block-decoration")
|
||||
@domNode.style.visibility = "hidden"
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
updateSync: (state) ->
|
||||
@newState = state.content
|
||||
@oldState ?= {blockDecorations: {}, width: 0}
|
||||
|
||||
if @newState.width isnt @oldState.width
|
||||
@domNode.style.width = @newState.width + "px"
|
||||
@oldState.width = @newState.width
|
||||
|
||||
for id, blockDecorationState of @oldState.blockDecorations
|
||||
unless @newState.blockDecorations.hasOwnProperty(id)
|
||||
@blockDecorationNodesById[id].remove()
|
||||
delete @blockDecorationNodesById[id]
|
||||
delete @oldState.blockDecorations[id]
|
||||
|
||||
for id, blockDecorationState of @newState.blockDecorations
|
||||
if @oldState.blockDecorations.hasOwnProperty(id)
|
||||
@updateBlockDecorationNode(id)
|
||||
else
|
||||
@oldState.blockDecorations[id] = {}
|
||||
@createAndAppendBlockDecorationNode(id)
|
||||
|
||||
measureBlockDecorations: ->
|
||||
for decorationId, blockDecorationNode of @blockDecorationNodesById
|
||||
style = getComputedStyle(blockDecorationNode)
|
||||
decoration = @newState.blockDecorations[decorationId].decoration
|
||||
marginBottom = parseInt(style.marginBottom) ? 0
|
||||
marginTop = parseInt(style.marginTop) ? 0
|
||||
@presenter.setBlockDecorationDimensions(
|
||||
decoration,
|
||||
blockDecorationNode.offsetWidth,
|
||||
blockDecorationNode.offsetHeight + marginTop + marginBottom
|
||||
)
|
||||
|
||||
createAndAppendBlockDecorationNode: (id) ->
|
||||
blockDecorationState = @newState.blockDecorations[id]
|
||||
blockDecorationNode = @views.getView(blockDecorationState.decoration.getProperties().item)
|
||||
blockDecorationNode.id = "atom--block-decoration-#{id}"
|
||||
@container.appendChild(blockDecorationNode)
|
||||
@blockDecorationNodesById[id] = blockDecorationNode
|
||||
@updateBlockDecorationNode(id)
|
||||
|
||||
updateBlockDecorationNode: (id) ->
|
||||
newBlockDecorationState = @newState.blockDecorations[id]
|
||||
oldBlockDecorationState = @oldState.blockDecorations[id]
|
||||
blockDecorationNode = @blockDecorationNodesById[id]
|
||||
|
||||
if newBlockDecorationState.isVisible
|
||||
blockDecorationNode.classList.remove("atom--invisible-block-decoration")
|
||||
else
|
||||
blockDecorationNode.classList.add("atom--invisible-block-decoration")
|
||||
|
||||
if oldBlockDecorationState.screenRow isnt newBlockDecorationState.screenRow
|
||||
blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
oldBlockDecorationState.screenRow = newBlockDecorationState.screenRow
|
||||
@@ -1,172 +0,0 @@
|
||||
{app, Menu} = require 'electron'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
# Used to manage the global application menu.
|
||||
#
|
||||
# It's created by {AtomApplication} upon instantiation and used to add, remove
|
||||
# and maintain the state of all menu items.
|
||||
module.exports =
|
||||
class ApplicationMenu
|
||||
constructor: (@version, @autoUpdateManager) ->
|
||||
@windowTemplates = new WeakMap()
|
||||
@setActiveTemplate(@getDefaultTemplate())
|
||||
@autoUpdateManager.on 'state-changed', (state) => @showUpdateMenuItem(state)
|
||||
|
||||
# Public: Updates the entire menu with the given keybindings.
|
||||
#
|
||||
# window - The BrowserWindow this menu template is associated with.
|
||||
# template - The Object which describes the menu to display.
|
||||
# keystrokesByCommand - An Object where the keys are commands and the values
|
||||
# are Arrays containing the keystroke.
|
||||
update: (window, template, keystrokesByCommand) ->
|
||||
@translateTemplate(template, keystrokesByCommand)
|
||||
@substituteVersion(template)
|
||||
@windowTemplates.set(window, template)
|
||||
@setActiveTemplate(template) if window is @lastFocusedWindow
|
||||
|
||||
setActiveTemplate: (template) ->
|
||||
unless _.isEqual(template, @activeTemplate)
|
||||
@activeTemplate = template
|
||||
@menu = Menu.buildFromTemplate(_.deepClone(template))
|
||||
Menu.setApplicationMenu(@menu)
|
||||
|
||||
@showUpdateMenuItem(@autoUpdateManager.getState())
|
||||
|
||||
# Register a BrowserWindow with this application menu.
|
||||
addWindow: (window) ->
|
||||
@lastFocusedWindow ?= window
|
||||
|
||||
focusHandler = =>
|
||||
@lastFocusedWindow = window
|
||||
if template = @windowTemplates.get(window)
|
||||
@setActiveTemplate(template)
|
||||
|
||||
window.on 'focus', focusHandler
|
||||
window.once 'closed', =>
|
||||
@lastFocusedWindow = null if window is @lastFocusedWindow
|
||||
@windowTemplates.delete(window)
|
||||
window.removeListener 'focus', focusHandler
|
||||
|
||||
@enableWindowSpecificItems(true)
|
||||
|
||||
# Flattens the given menu and submenu items into an single Array.
|
||||
#
|
||||
# menu - A complete menu configuration object for atom-shell's menu API.
|
||||
#
|
||||
# Returns an Array of native menu items.
|
||||
flattenMenuItems: (menu) ->
|
||||
items = []
|
||||
for index, item of menu.items or {}
|
||||
items.push(item)
|
||||
items = items.concat(@flattenMenuItems(item.submenu)) if item.submenu
|
||||
items
|
||||
|
||||
# Flattens the given menu template into an single Array.
|
||||
#
|
||||
# template - An object describing the menu item.
|
||||
#
|
||||
# Returns an Array of native menu items.
|
||||
flattenMenuTemplate: (template) ->
|
||||
items = []
|
||||
for item in template
|
||||
items.push(item)
|
||||
items = items.concat(@flattenMenuTemplate(item.submenu)) if item.submenu
|
||||
items
|
||||
|
||||
# Public: Used to make all window related menu items are active.
|
||||
#
|
||||
# enable - If true enables all window specific items, if false disables all
|
||||
# window specific items.
|
||||
enableWindowSpecificItems: (enable) ->
|
||||
for item in @flattenMenuItems(@menu)
|
||||
item.enabled = enable if item.metadata?.windowSpecific
|
||||
return
|
||||
|
||||
# Replaces VERSION with the current version.
|
||||
substituteVersion: (template) ->
|
||||
if (item = _.find(@flattenMenuTemplate(template), ({label}) -> label is 'VERSION'))
|
||||
item.label = "Version #{@version}"
|
||||
|
||||
# Sets the proper visible state the update menu items
|
||||
showUpdateMenuItem: (state) ->
|
||||
checkForUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Check for Update')
|
||||
checkingForUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Checking for Update')
|
||||
downloadingUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Downloading Update')
|
||||
installUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Restart and Install Update')
|
||||
|
||||
return unless checkForUpdateItem? and checkingForUpdateItem? and downloadingUpdateItem? and installUpdateItem?
|
||||
|
||||
checkForUpdateItem.visible = false
|
||||
checkingForUpdateItem.visible = false
|
||||
downloadingUpdateItem.visible = false
|
||||
installUpdateItem.visible = false
|
||||
|
||||
switch state
|
||||
when 'idle', 'error', 'no-update-available'
|
||||
checkForUpdateItem.visible = true
|
||||
when 'checking'
|
||||
checkingForUpdateItem.visible = true
|
||||
when 'downloading'
|
||||
downloadingUpdateItem.visible = true
|
||||
when 'update-available'
|
||||
installUpdateItem.visible = true
|
||||
|
||||
# Default list of menu items.
|
||||
#
|
||||
# Returns an Array of menu item Objects.
|
||||
getDefaultTemplate: ->
|
||||
[
|
||||
label: "Atom"
|
||||
submenu: [
|
||||
{label: "Check for Update", metadata: {autoUpdate: true}}
|
||||
{label: 'Reload', accelerator: 'Command+R', click: => @focusedWindow()?.reload()}
|
||||
{label: 'Close Window', accelerator: 'Command+Shift+W', click: => @focusedWindow()?.close()}
|
||||
{label: 'Toggle Dev Tools', accelerator: 'Command+Alt+I', click: => @focusedWindow()?.toggleDevTools()}
|
||||
{label: 'Quit', accelerator: 'Command+Q', click: -> app.quit()}
|
||||
]
|
||||
]
|
||||
|
||||
focusedWindow: ->
|
||||
_.find global.atomApplication.windows, (atomWindow) -> atomWindow.isFocused()
|
||||
|
||||
# Combines a menu template with the appropriate keystroke.
|
||||
#
|
||||
# template - An Object conforming to atom-shell's menu api but lacking
|
||||
# accelerator and click properties.
|
||||
# keystrokesByCommand - An Object where the keys are commands and the values
|
||||
# are Arrays containing the keystroke.
|
||||
#
|
||||
# Returns a complete menu configuration object for atom-shell's menu API.
|
||||
translateTemplate: (template, keystrokesByCommand) ->
|
||||
template.forEach (item) =>
|
||||
item.metadata ?= {}
|
||||
if item.command
|
||||
item.accelerator = @acceleratorForCommand(item.command, keystrokesByCommand)
|
||||
item.click = -> global.atomApplication.sendCommand(item.command)
|
||||
item.metadata.windowSpecific = true unless /^application:/.test(item.command)
|
||||
@translateTemplate(item.submenu, keystrokesByCommand) if item.submenu
|
||||
template
|
||||
|
||||
# Determine the accelerator for a given command.
|
||||
#
|
||||
# command - The name of the command.
|
||||
# keystrokesByCommand - An Object where the keys are commands and the values
|
||||
# are Arrays containing the keystroke.
|
||||
#
|
||||
# Returns a String containing the keystroke in a format that can be interpreted
|
||||
# by atom shell to provide nice icons where available.
|
||||
acceleratorForCommand: (command, keystrokesByCommand) ->
|
||||
firstKeystroke = keystrokesByCommand[command]?[0]
|
||||
return null unless firstKeystroke
|
||||
|
||||
modifiers = firstKeystroke.split(/-(?=.)/)
|
||||
key = modifiers.pop().toUpperCase().replace('+', 'Plus')
|
||||
|
||||
modifiers = modifiers.map (modifier) ->
|
||||
modifier.replace(/shift/ig, "Shift")
|
||||
.replace(/cmd/ig, "Command")
|
||||
.replace(/ctrl/ig, "Ctrl")
|
||||
.replace(/alt/ig, "Alt")
|
||||
|
||||
keys = modifiers.concat([key])
|
||||
keys.join("+")
|
||||
@@ -1,680 +0,0 @@
|
||||
AtomWindow = require './atom-window'
|
||||
ApplicationMenu = require './application-menu'
|
||||
AtomProtocolHandler = require './atom-protocol-handler'
|
||||
AutoUpdateManager = require './auto-update-manager'
|
||||
StorageFolder = require '../storage-folder'
|
||||
ipcHelpers = require '../ipc-helpers'
|
||||
{BrowserWindow, Menu, app, dialog, ipcMain, shell} = require 'electron'
|
||||
fs = require 'fs-plus'
|
||||
path = require 'path'
|
||||
os = require 'os'
|
||||
net = require 'net'
|
||||
url = require 'url'
|
||||
{EventEmitter} = require 'events'
|
||||
_ = require 'underscore-plus'
|
||||
FindParentDir = null
|
||||
Resolve = null
|
||||
|
||||
LocationSuffixRegExp = /(:\d+)(:\d+)?$/
|
||||
|
||||
# The application's singleton class.
|
||||
#
|
||||
# It's the entry point into the Atom application and maintains the global state
|
||||
# of the application.
|
||||
#
|
||||
module.exports =
|
||||
class AtomApplication
|
||||
_.extend @prototype, EventEmitter.prototype
|
||||
|
||||
# Public: The entry point into the Atom application.
|
||||
@open: (options) ->
|
||||
unless options.socketPath?
|
||||
if process.platform is 'win32'
|
||||
options.socketPath = '\\\\.\\pipe\\atom-sock'
|
||||
else
|
||||
options.socketPath = path.join(os.tmpdir(), "atom-#{options.version}-#{process.env.USER}.sock")
|
||||
|
||||
createAtomApplication = -> new AtomApplication(options)
|
||||
|
||||
# FIXME: Sometimes when socketPath doesn't exist, net.connect would strangely
|
||||
# take a few seconds to trigger 'error' event, it could be a bug of node
|
||||
# or atom-shell, before it's fixed we check the existence of socketPath to
|
||||
# speedup startup.
|
||||
if (process.platform isnt 'win32' and not fs.existsSync options.socketPath) or options.test
|
||||
createAtomApplication()
|
||||
return
|
||||
|
||||
client = net.connect {path: options.socketPath}, ->
|
||||
client.write JSON.stringify(options), ->
|
||||
client.end()
|
||||
app.quit()
|
||||
|
||||
client.on 'error', createAtomApplication
|
||||
|
||||
windows: null
|
||||
applicationMenu: null
|
||||
atomProtocolHandler: null
|
||||
resourcePath: null
|
||||
version: null
|
||||
quitting: false
|
||||
|
||||
exit: (status) -> app.exit(status)
|
||||
|
||||
constructor: (options) ->
|
||||
{@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, timeout, clearWindowState} = options
|
||||
|
||||
@socketPath = null if options.test
|
||||
|
||||
global.atomApplication = this
|
||||
|
||||
@pidsToOpenWindows = {}
|
||||
@windows = []
|
||||
|
||||
@autoUpdateManager = new AutoUpdateManager(@version, options.test, @resourcePath)
|
||||
@applicationMenu = new ApplicationMenu(@version, @autoUpdateManager)
|
||||
@atomProtocolHandler = new AtomProtocolHandler(@resourcePath, @safeMode)
|
||||
|
||||
@listenForArgumentsFromNewProcess()
|
||||
@setupJavaScriptArguments()
|
||||
@handleEvents()
|
||||
@setupDockMenu()
|
||||
@storageFolder = new StorageFolder(process.env.ATOM_HOME)
|
||||
|
||||
if options.pathsToOpen?.length > 0 or options.urlsToOpen?.length > 0 or options.test
|
||||
@openWithOptions(options)
|
||||
else
|
||||
@loadState(options) or @openPath(options)
|
||||
|
||||
openWithOptions: ({initialPaths, pathsToOpen, executedFrom, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, profileStartup, timeout, clearWindowState, addToLastWindow, env}) ->
|
||||
if test
|
||||
@runTests({headless: true, devMode, @resourcePath, executedFrom, pathsToOpen, logFile, timeout, env})
|
||||
else if pathsToOpen.length > 0
|
||||
@openPaths({initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState, addToLastWindow, env})
|
||||
else if urlsToOpen.length > 0
|
||||
@openUrl({urlToOpen, devMode, safeMode, env}) for urlToOpen in urlsToOpen
|
||||
else
|
||||
# Always open a editor window if this is the first instance of Atom.
|
||||
@openPath({initialPaths, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState, addToLastWindow, env})
|
||||
|
||||
# Public: Removes the {AtomWindow} from the global window list.
|
||||
removeWindow: (window) ->
|
||||
if @windows.length is 1
|
||||
@applicationMenu?.enableWindowSpecificItems(false)
|
||||
if process.platform in ['win32', 'linux']
|
||||
app.quit()
|
||||
return
|
||||
@windows.splice(@windows.indexOf(window), 1)
|
||||
@saveState(true) unless window.isSpec
|
||||
|
||||
# Public: Adds the {AtomWindow} to the global window list.
|
||||
addWindow: (window) ->
|
||||
@windows.push window
|
||||
@applicationMenu?.addWindow(window.browserWindow)
|
||||
window.once 'window:loaded', =>
|
||||
@autoUpdateManager.emitUpdateAvailableEvent(window)
|
||||
|
||||
unless window.isSpec
|
||||
focusHandler = => @lastFocusedWindow = window
|
||||
blurHandler = => @saveState(false)
|
||||
window.browserWindow.on 'focus', focusHandler
|
||||
window.browserWindow.on 'blur', blurHandler
|
||||
window.browserWindow.once 'closed', =>
|
||||
@lastFocusedWindow = null if window is @lastFocusedWindow
|
||||
window.browserWindow.removeListener 'focus', focusHandler
|
||||
window.browserWindow.removeListener 'blur', blurHandler
|
||||
window.browserWindow.webContents.once 'did-finish-load', => @saveState(false)
|
||||
|
||||
# Creates server to listen for additional atom application launches.
|
||||
#
|
||||
# You can run the atom command multiple times, but after the first launch
|
||||
# the other launches will just pass their information to this server and then
|
||||
# close immediately.
|
||||
listenForArgumentsFromNewProcess: ->
|
||||
return unless @socketPath?
|
||||
@deleteSocketFile()
|
||||
server = net.createServer (connection) =>
|
||||
connection.on 'data', (data) =>
|
||||
options = JSON.parse(data)
|
||||
@openWithOptions(options)
|
||||
|
||||
server.listen @socketPath
|
||||
server.on 'error', (error) -> console.error 'Application server failed', error
|
||||
|
||||
deleteSocketFile: ->
|
||||
return if process.platform is 'win32' or not @socketPath?
|
||||
|
||||
if fs.existsSync(@socketPath)
|
||||
try
|
||||
fs.unlinkSync(@socketPath)
|
||||
catch error
|
||||
# Ignore ENOENT errors in case the file was deleted between the exists
|
||||
# check and the call to unlink sync. This occurred occasionally on CI
|
||||
# which is why this check is here.
|
||||
throw error unless error.code is 'ENOENT'
|
||||
|
||||
# Configures required javascript environment flags.
|
||||
setupJavaScriptArguments: ->
|
||||
app.commandLine.appendSwitch 'js-flags', '--harmony'
|
||||
|
||||
# Registers basic application commands, non-idempotent.
|
||||
handleEvents: ->
|
||||
getLoadSettings = =>
|
||||
devMode: @focusedWindow()?.devMode
|
||||
safeMode: @focusedWindow()?.safeMode
|
||||
|
||||
@on 'application:quit', -> app.quit()
|
||||
@on 'application:new-window', -> @openPath(getLoadSettings())
|
||||
@on 'application:new-file', -> (@focusedWindow() ? this).openPath()
|
||||
@on 'application:open', -> @promptForPathToOpen('all', getLoadSettings())
|
||||
@on 'application:open-file', -> @promptForPathToOpen('file', getLoadSettings())
|
||||
@on 'application:open-folder', -> @promptForPathToOpen('folder', getLoadSettings())
|
||||
@on 'application:open-dev', -> @promptForPathToOpen('all', devMode: true)
|
||||
@on 'application:open-safe', -> @promptForPathToOpen('all', safeMode: true)
|
||||
@on 'application:inspect', ({x, y, atomWindow}) ->
|
||||
atomWindow ?= @focusedWindow()
|
||||
atomWindow?.browserWindow.inspectElement(x, y)
|
||||
|
||||
@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-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')
|
||||
@on 'application:search-issues', -> shell.openExternal('https://github.com/issues?q=+is%3Aissue+user%3Aatom')
|
||||
|
||||
@on 'application:install-update', =>
|
||||
@quitting = true
|
||||
@autoUpdateManager.install()
|
||||
|
||||
@on 'application:check-for-update', => @autoUpdateManager.check()
|
||||
|
||||
if process.platform is 'darwin'
|
||||
@on 'application:bring-all-windows-to-front', -> Menu.sendActionToFirstResponder('arrangeInFront:')
|
||||
@on 'application:hide', -> Menu.sendActionToFirstResponder('hide:')
|
||||
@on 'application:hide-other-applications', -> Menu.sendActionToFirstResponder('hideOtherApplications:')
|
||||
@on 'application:minimize', -> Menu.sendActionToFirstResponder('performMiniaturize:')
|
||||
@on 'application:unhide-all-applications', -> Menu.sendActionToFirstResponder('unhideAllApplications:')
|
||||
@on 'application:zoom', -> Menu.sendActionToFirstResponder('zoom:')
|
||||
else
|
||||
@on 'application:minimize', -> @focusedWindow()?.minimize()
|
||||
@on 'application:zoom', -> @focusedWindow()?.maximize()
|
||||
|
||||
@openPathOnEvent('application:about', 'atom://about')
|
||||
@openPathOnEvent('application:show-settings', 'atom://config')
|
||||
@openPathOnEvent('application:open-your-config', 'atom://.atom/config')
|
||||
@openPathOnEvent('application:open-your-init-script', 'atom://.atom/init-script')
|
||||
@openPathOnEvent('application:open-your-keymap', 'atom://.atom/keymap')
|
||||
@openPathOnEvent('application:open-your-snippets', 'atom://.atom/snippets')
|
||||
@openPathOnEvent('application:open-your-stylesheet', 'atom://.atom/stylesheet')
|
||||
@openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md'))
|
||||
|
||||
app.on 'before-quit', =>
|
||||
@quitting = true
|
||||
|
||||
app.on 'will-quit', =>
|
||||
@killAllProcesses()
|
||||
@deleteSocketFile()
|
||||
|
||||
app.on 'open-file', (event, pathToOpen) =>
|
||||
event.preventDefault()
|
||||
@openPath({pathToOpen})
|
||||
|
||||
app.on 'open-url', (event, urlToOpen) =>
|
||||
event.preventDefault()
|
||||
@openUrl({urlToOpen, @devMode, @safeMode})
|
||||
|
||||
app.on 'activate-with-no-open-windows', (event) =>
|
||||
event?.preventDefault()
|
||||
@emit('application:new-window')
|
||||
|
||||
# A request from the associated render process to open a new render process.
|
||||
ipcMain.on 'open', (event, options) =>
|
||||
window = @windowForEvent(event)
|
||||
if options?
|
||||
if typeof options.pathsToOpen is 'string'
|
||||
options.pathsToOpen = [options.pathsToOpen]
|
||||
if options.pathsToOpen?.length > 0
|
||||
options.window = window
|
||||
@openPaths(options)
|
||||
else
|
||||
new AtomWindow(options)
|
||||
else
|
||||
@promptForPathToOpen('all', {window})
|
||||
|
||||
ipcMain.on 'update-application-menu', (event, template, keystrokesByCommand) =>
|
||||
win = BrowserWindow.fromWebContents(event.sender)
|
||||
@applicationMenu.update(win, template, keystrokesByCommand)
|
||||
|
||||
ipcMain.on 'run-package-specs', (event, packageSpecPath) =>
|
||||
@runTests({resourcePath: @devResourcePath, pathsToOpen: [packageSpecPath], headless: false})
|
||||
|
||||
ipcMain.on 'command', (event, command) =>
|
||||
@emit(command)
|
||||
|
||||
ipcMain.on 'window-command', (event, command, args...) ->
|
||||
win = BrowserWindow.fromWebContents(event.sender)
|
||||
win.emit(command, args...)
|
||||
|
||||
ipcMain.on 'call-window-method', (event, method, args...) ->
|
||||
win = BrowserWindow.fromWebContents(event.sender)
|
||||
win[method](args...)
|
||||
|
||||
ipcMain.on 'pick-folder', (event, responseChannel) =>
|
||||
@promptForPath "folder", (selectedPaths) ->
|
||||
event.sender.send(responseChannel, selectedPaths)
|
||||
|
||||
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'
|
||||
ipcMain.on 'write-text-to-selection-clipboard', (event, selectedText) ->
|
||||
clipboard.writeText(selectedText, 'selection')
|
||||
|
||||
ipcMain.on 'write-to-stdout', (event, output) ->
|
||||
process.stdout.write(output)
|
||||
|
||||
ipcMain.on 'write-to-stderr', (event, output) ->
|
||||
process.stderr.write(output)
|
||||
|
||||
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 [
|
||||
{label: 'New Window', click: => @emit('application:new-window')}
|
||||
]
|
||||
app.dock.setMenu dockMenu
|
||||
|
||||
# Public: Executes the given command.
|
||||
#
|
||||
# If it isn't handled globally, delegate to the currently focused window.
|
||||
#
|
||||
# command - The string representing the command.
|
||||
# args - The optional arguments to pass along.
|
||||
sendCommand: (command, args...) ->
|
||||
unless @emit(command, args...)
|
||||
focusedWindow = @focusedWindow()
|
||||
if focusedWindow?
|
||||
focusedWindow.sendCommand(command, args...)
|
||||
else
|
||||
@sendCommandToFirstResponder(command)
|
||||
|
||||
# Public: Executes the given command on the given window.
|
||||
#
|
||||
# command - The string representing the command.
|
||||
# atomWindow - The {AtomWindow} to send the command to.
|
||||
# args - The optional arguments to pass along.
|
||||
sendCommandToWindow: (command, atomWindow, args...) ->
|
||||
unless @emit(command, args...)
|
||||
if atomWindow?
|
||||
atomWindow.sendCommand(command, args...)
|
||||
else
|
||||
@sendCommandToFirstResponder(command)
|
||||
|
||||
# Translates the command into OS X action and sends it to application's first
|
||||
# responder.
|
||||
sendCommandToFirstResponder: (command) ->
|
||||
return false unless process.platform is 'darwin'
|
||||
|
||||
switch command
|
||||
when 'core:undo' then Menu.sendActionToFirstResponder('undo:')
|
||||
when 'core:redo' then Menu.sendActionToFirstResponder('redo:')
|
||||
when 'core:copy' then Menu.sendActionToFirstResponder('copy:')
|
||||
when 'core:cut' then Menu.sendActionToFirstResponder('cut:')
|
||||
when 'core:paste' then Menu.sendActionToFirstResponder('paste:')
|
||||
when 'core:select-all' then Menu.sendActionToFirstResponder('selectAll:')
|
||||
else return false
|
||||
true
|
||||
|
||||
# Public: Open the given path in the focused window when the event is
|
||||
# triggered.
|
||||
#
|
||||
# A new window will be created if there is no currently focused window.
|
||||
#
|
||||
# eventName - The event to listen for.
|
||||
# pathToOpen - The path to open when the event is triggered.
|
||||
openPathOnEvent: (eventName, pathToOpen) ->
|
||||
@on eventName, ->
|
||||
if window = @focusedWindow()
|
||||
window.openPath(pathToOpen)
|
||||
else
|
||||
@openPath({pathToOpen})
|
||||
|
||||
# Returns the {AtomWindow} for the given paths.
|
||||
windowForPaths: (pathsToOpen, devMode) ->
|
||||
_.find @windows, (atomWindow) ->
|
||||
atomWindow.devMode is devMode and atomWindow.containsPaths(pathsToOpen)
|
||||
|
||||
# Returns the {AtomWindow} for the given ipcMain event.
|
||||
windowForEvent: ({sender}) ->
|
||||
window = BrowserWindow.fromWebContents(sender)
|
||||
_.find @windows, ({browserWindow}) -> window is browserWindow
|
||||
|
||||
# Public: Returns the currently focused {AtomWindow} or undefined if none.
|
||||
focusedWindow: ->
|
||||
_.find @windows, (atomWindow) -> atomWindow.isFocused()
|
||||
|
||||
# Get the platform-specific window offset for new windows.
|
||||
getWindowOffsetForCurrentPlatform: ->
|
||||
offsetByPlatform =
|
||||
darwin: 22
|
||||
win32: 26
|
||||
offsetByPlatform[process.platform] ? 0
|
||||
|
||||
# Get the dimensions for opening a new window by cascading as appropriate to
|
||||
# the platform.
|
||||
getDimensionsForNewWindow: ->
|
||||
return if (@focusedWindow() ? @lastFocusedWindow)?.isMaximized()
|
||||
dimensions = (@focusedWindow() ? @lastFocusedWindow)?.getDimensions()
|
||||
offset = @getWindowOffsetForCurrentPlatform()
|
||||
if dimensions? and offset?
|
||||
dimensions.x += offset
|
||||
dimensions.y += offset
|
||||
dimensions
|
||||
|
||||
# Public: Opens a single path, in an existing window if possible.
|
||||
#
|
||||
# options -
|
||||
# :pathToOpen - The file path to open
|
||||
# :pidToKillWhenClosed - The integer of the pid to kill
|
||||
# :newWindow - Boolean of whether this should be opened in a new window.
|
||||
# :devMode - Boolean to control the opened window's dev mode.
|
||||
# :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.
|
||||
# :addToLastWindow - Boolean of whether this should be opened in last focused window.
|
||||
openPath: ({initialPaths, pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow, env} = {}) ->
|
||||
@openPaths({initialPaths, pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow, env})
|
||||
|
||||
# Public: Opens multiple paths, in existing windows if possible.
|
||||
#
|
||||
# options -
|
||||
# :pathsToOpen - The array of file paths to open
|
||||
# :pidToKillWhenClosed - The integer of the pid to kill
|
||||
# :newWindow - Boolean of whether this should be opened in a new window.
|
||||
# :devMode - Boolean to control the opened window's dev mode.
|
||||
# :safeMode - Boolean to control the opened window's safe mode.
|
||||
# :windowDimensions - Object with height and width keys.
|
||||
# :window - {AtomWindow} to open file paths in.
|
||||
# :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, env}={}) ->
|
||||
devMode = Boolean(devMode)
|
||||
safeMode = Boolean(safeMode)
|
||||
clearWindowState = Boolean(clearWindowState)
|
||||
locationsToOpen = (@locationForPathToOpen(pathToOpen, executedFrom, addToLastWindow) for pathToOpen in pathsToOpen)
|
||||
pathsToOpen = (locationToOpen.pathToOpen for locationToOpen in locationsToOpen)
|
||||
|
||||
unless pidToKillWhenClosed or newWindow
|
||||
existingWindow = @windowForPaths(pathsToOpen, devMode)
|
||||
stats = (fs.statSyncNoException(pathToOpen) for pathToOpen in pathsToOpen)
|
||||
unless existingWindow?
|
||||
if currentWindow = window ? @lastFocusedWindow
|
||||
existingWindow = currentWindow if (
|
||||
addToLastWindow or
|
||||
currentWindow.devMode is devMode and
|
||||
(
|
||||
stats.every((stat) -> stat.isFile?()) or
|
||||
stats.some((stat) -> stat.isDirectory?() and not currentWindow.hasProjectPath())
|
||||
)
|
||||
)
|
||||
|
||||
if existingWindow?
|
||||
openedWindow = existingWindow
|
||||
openedWindow.openLocations(locationsToOpen)
|
||||
if openedWindow.isMinimized()
|
||||
openedWindow.restore()
|
||||
else
|
||||
openedWindow.focus()
|
||||
else
|
||||
if devMode
|
||||
try
|
||||
windowInitializationScript = require.resolve(path.join(@devResourcePath, 'src', 'initialize-application-window'))
|
||||
resourcePath = @devResourcePath
|
||||
|
||||
windowInitializationScript ?= require.resolve('../initialize-application-window')
|
||||
resourcePath ?= @resourcePath
|
||||
windowDimensions ?= @getDimensionsForNewWindow()
|
||||
openedWindow = new AtomWindow({initialPaths, locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState, env})
|
||||
|
||||
if pidToKillWhenClosed?
|
||||
@pidsToOpenWindows[pidToKillWhenClosed] = openedWindow
|
||||
|
||||
openedWindow.browserWindow.once 'closed', =>
|
||||
@killProcessForWindow(openedWindow)
|
||||
|
||||
# Kill all processes associated with opened windows.
|
||||
killAllProcesses: ->
|
||||
@killProcess(pid) for pid of @pidsToOpenWindows
|
||||
return
|
||||
|
||||
# Kill process associated with the given opened window.
|
||||
killProcessForWindow: (openedWindow) ->
|
||||
for pid, trackedWindow of @pidsToOpenWindows
|
||||
@killProcess(pid) if trackedWindow is openedWindow
|
||||
return
|
||||
|
||||
# Kill the process with the given pid.
|
||||
killProcess: (pid) ->
|
||||
try
|
||||
parsedPid = parseInt(pid)
|
||||
process.kill(parsedPid) if isFinite(parsedPid)
|
||||
catch error
|
||||
if error.code isnt 'ESRCH'
|
||||
console.log("Killing process #{pid} failed: #{error.code ? error.message}")
|
||||
delete @pidsToOpenWindows[pid]
|
||||
|
||||
saveState: (allowEmpty=false) ->
|
||||
return if @quitting
|
||||
states = []
|
||||
for window in @windows
|
||||
unless window.isSpec
|
||||
if loadSettings = window.getLoadSettings()
|
||||
states.push(initialPaths: loadSettings.initialPaths)
|
||||
if states.length > 0 or allowEmpty
|
||||
@storageFolder.storeSync('application.json', states)
|
||||
|
||||
loadState: (options) ->
|
||||
if (states = @storageFolder.load('application.json'))?.length > 0
|
||||
for state in states
|
||||
@openWithOptions(_.extend(options, {
|
||||
initialPaths: state.initialPaths
|
||||
pathsToOpen: state.initialPaths.filter (directoryPath) -> fs.isDirectorySync(directoryPath)
|
||||
urlsToOpen: []
|
||||
devMode: @devMode
|
||||
safeMode: @safeMode
|
||||
}))
|
||||
true
|
||||
else
|
||||
false
|
||||
|
||||
# Open an atom:// url.
|
||||
#
|
||||
# The host of the URL being opened is assumed to be the package name
|
||||
# responsible for opening the URL. A new window will be created with
|
||||
# that package's `urlMain` as the bootstrap script.
|
||||
#
|
||||
# options -
|
||||
# :urlToOpen - The atom:// url to open.
|
||||
# :devMode - Boolean to control the opened window's dev mode.
|
||||
# :safeMode - Boolean to control the opened window's safe mode.
|
||||
openUrl: ({urlToOpen, devMode, safeMode, env}) ->
|
||||
unless @packages?
|
||||
PackageManager = require '../package-manager'
|
||||
@packages = new PackageManager
|
||||
configDirPath: process.env.ATOM_HOME
|
||||
devMode: devMode
|
||||
resourcePath: @resourcePath
|
||||
|
||||
packageName = url.parse(urlToOpen).host
|
||||
pack = _.find @packages.getAvailablePackageMetadata(), ({name}) -> name is packageName
|
||||
if pack?
|
||||
if pack.urlMain
|
||||
packagePath = @packages.resolvePackagePath(packageName)
|
||||
windowInitializationScript = path.resolve(packagePath, pack.urlMain)
|
||||
windowDimensions = @getDimensionsForNewWindow()
|
||||
new AtomWindow({windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env})
|
||||
else
|
||||
console.log "Package '#{pack.name}' does not have a url main: #{urlToOpen}"
|
||||
else
|
||||
console.log "Opening unknown url: #{urlToOpen}"
|
||||
|
||||
# Opens up a new {AtomWindow} to run specs within.
|
||||
#
|
||||
# options -
|
||||
# :headless - A Boolean that, if true, will close the window upon
|
||||
# completion.
|
||||
# :resourcePath - The path to include specs from.
|
||||
# :specPath - The directory to load specs from.
|
||||
# :safeMode - A Boolean that, if true, won't run specs from ~/.atom/packages
|
||||
# and ~/.atom/dev/packages, defaults to false.
|
||||
runTests: ({headless, resourcePath, executedFrom, pathsToOpen, logFile, safeMode, timeout, env}) ->
|
||||
if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath)
|
||||
resourcePath = @resourcePath
|
||||
|
||||
timeoutInSeconds = Number.parseFloat(timeout)
|
||||
unless Number.isNaN(timeoutInSeconds)
|
||||
timeoutHandler = ->
|
||||
console.log "The test suite has timed out because it has been running for more than #{timeoutInSeconds} seconds."
|
||||
process.exit(124) # Use the same exit code as the UNIX timeout util.
|
||||
setTimeout(timeoutHandler, timeoutInSeconds * 1000)
|
||||
|
||||
try
|
||||
windowInitializationScript = require.resolve(path.resolve(@devResourcePath, 'src', 'initialize-test-window'))
|
||||
catch error
|
||||
windowInitializationScript = require.resolve(path.resolve(__dirname, '..', '..', 'src', 'initialize-test-window'))
|
||||
|
||||
testPaths = []
|
||||
if pathsToOpen?
|
||||
for pathToOpen in pathsToOpen
|
||||
testPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen)))
|
||||
|
||||
if testPaths.length is 0
|
||||
process.stderr.write 'Error: Specify at least one test path\n\n'
|
||||
process.exit(1)
|
||||
|
||||
legacyTestRunnerPath = @resolveLegacyTestRunnerPath()
|
||||
testRunnerPath = @resolveTestRunnerPath(testPaths[0])
|
||||
devMode = true
|
||||
isSpec = true
|
||||
safeMode ?= false
|
||||
new AtomWindow({windowInitializationScript, resourcePath, headless, isSpec, devMode, testRunnerPath, legacyTestRunnerPath, testPaths, logFile, safeMode, env})
|
||||
|
||||
resolveTestRunnerPath: (testPath) ->
|
||||
FindParentDir ?= require 'find-parent-dir'
|
||||
|
||||
if packageRoot = FindParentDir.sync(testPath, 'package.json')
|
||||
packageMetadata = require(path.join(packageRoot, 'package.json'))
|
||||
if packageMetadata.atomTestRunner
|
||||
Resolve ?= require('resolve')
|
||||
if testRunnerPath = Resolve.sync(packageMetadata.atomTestRunner, basedir: packageRoot, extensions: Object.keys(require.extensions))
|
||||
return testRunnerPath
|
||||
else
|
||||
process.stderr.write "Error: Could not resolve test runner path '#{packageMetadata.atomTestRunner}'"
|
||||
process.exit(1)
|
||||
|
||||
@resolveLegacyTestRunnerPath()
|
||||
|
||||
resolveLegacyTestRunnerPath: ->
|
||||
try
|
||||
require.resolve(path.resolve(@devResourcePath, 'spec', 'jasmine-test-runner'))
|
||||
catch error
|
||||
require.resolve(path.resolve(__dirname, '..', '..', 'spec', 'jasmine-test-runner'))
|
||||
|
||||
locationForPathToOpen: (pathToOpen, executedFrom='', forceAddToWindow) ->
|
||||
return {pathToOpen} unless pathToOpen
|
||||
|
||||
pathToOpen = pathToOpen.replace(/[:\s]+$/, '')
|
||||
match = pathToOpen.match(LocationSuffixRegExp)
|
||||
|
||||
if match?
|
||||
pathToOpen = pathToOpen.slice(0, -match[0].length)
|
||||
initialLine = Math.max(0, parseInt(match[1].slice(1)) - 1) if match[1]
|
||||
initialColumn = Math.max(0, parseInt(match[2].slice(1)) - 1) if match[2]
|
||||
else
|
||||
initialLine = initialColumn = null
|
||||
|
||||
unless url.parse(pathToOpen).protocol?
|
||||
pathToOpen = path.resolve(executedFrom, fs.normalize(pathToOpen))
|
||||
|
||||
{pathToOpen, initialLine, initialColumn, forceAddToWindow}
|
||||
|
||||
# Opens a native dialog to prompt the user for a path.
|
||||
#
|
||||
# Once paths are selected, they're opened in a new or existing {AtomWindow}s.
|
||||
#
|
||||
# options -
|
||||
# :type - A String which specifies the type of the dialog, could be 'file',
|
||||
# 'folder' or 'all'. The 'all' is only available on OS X.
|
||||
# :devMode - A Boolean which controls whether any newly opened windows
|
||||
# should be in dev mode or not.
|
||||
# :safeMode - A Boolean which controls whether any newly opened windows
|
||||
# should be in safe mode or not.
|
||||
# :window - An {AtomWindow} to use for opening a selected file path.
|
||||
promptForPathToOpen: (type, {devMode, safeMode, window}) ->
|
||||
@promptForPath type, (pathsToOpen) =>
|
||||
@openPaths({pathsToOpen, devMode, safeMode, window})
|
||||
|
||||
promptForPath: (type, callback) ->
|
||||
properties =
|
||||
switch type
|
||||
when 'file' then ['openFile']
|
||||
when 'folder' then ['openDirectory']
|
||||
when 'all' then ['openFile', 'openDirectory']
|
||||
else throw new Error("#{type} is an invalid type for promptForPath")
|
||||
|
||||
# Show the open dialog as child window on Windows and Linux, and as
|
||||
# independent dialog on OS X. This matches most native apps.
|
||||
parentWindow =
|
||||
if process.platform is 'darwin'
|
||||
null
|
||||
else
|
||||
BrowserWindow.getFocusedWindow()
|
||||
|
||||
openOptions =
|
||||
properties: properties.concat(['multiSelections', 'createDirectory'])
|
||||
title: switch type
|
||||
when 'file' then 'Open File'
|
||||
when 'folder' then 'Open Folder'
|
||||
else 'Open'
|
||||
|
||||
if process.platform is 'linux'
|
||||
if projectPath = @lastFocusedWindow?.projectPath
|
||||
openOptions.defaultPath = projectPath
|
||||
|
||||
dialog.showOpenDialog(parentWindow, openOptions, callback)
|
||||
@@ -1,35 +0,0 @@
|
||||
fs = require 'fs-plus'
|
||||
path = require 'path'
|
||||
{ipcMain} = require 'electron'
|
||||
|
||||
module.exports =
|
||||
class AtomPortable
|
||||
@getPortableAtomHomePath: ->
|
||||
execDirectoryPath = path.dirname(process.execPath)
|
||||
path.join(execDirectoryPath, '..', '.atom')
|
||||
|
||||
@setPortable: (existingAtomHome) ->
|
||||
fs.copySync(existingAtomHome, @getPortableAtomHomePath())
|
||||
|
||||
@isPortableInstall: (platform, environmentAtomHome, defaultHome) ->
|
||||
return false unless platform in ['linux', 'win32']
|
||||
return false if environmentAtomHome
|
||||
return false if not fs.existsSync(@getPortableAtomHomePath())
|
||||
# currently checking only that the directory exists and is writable,
|
||||
# probably want to do some integrity checks on contents in future
|
||||
@isPortableAtomHomePathWritable(defaultHome)
|
||||
|
||||
@isPortableAtomHomePathWritable: (defaultHome) ->
|
||||
writable = false
|
||||
message = ""
|
||||
try
|
||||
writePermissionTestFile = path.join(@getPortableAtomHomePath(), "write.test")
|
||||
fs.writeFileSync(writePermissionTestFile, "test") if not fs.existsSync(writePermissionTestFile)
|
||||
fs.removeSync(writePermissionTestFile)
|
||||
writable = true
|
||||
catch error
|
||||
message = "Failed to use portable Atom home directory (#{@getPortableAtomHomePath()}). Using the default instead (#{defaultHome}). #{error.message}"
|
||||
|
||||
ipcMain.on 'check-portable-home-writable', (event) ->
|
||||
event.sender.send 'check-portable-home-writable-response', {writable, message}
|
||||
writable
|
||||
@@ -1,43 +0,0 @@
|
||||
{app, protocol} = require 'electron'
|
||||
fs = require 'fs'
|
||||
path = require 'path'
|
||||
|
||||
# Handles requests with 'atom' protocol.
|
||||
#
|
||||
# It's created by {AtomApplication} upon instantiation and is used to create a
|
||||
# custom resource loader for 'atom://' URLs.
|
||||
#
|
||||
# The following directories are searched in order:
|
||||
# * ~/.atom/assets
|
||||
# * ~/.atom/dev/packages (unless in safe mode)
|
||||
# * ~/.atom/packages
|
||||
# * RESOURCE_PATH/node_modules
|
||||
#
|
||||
module.exports =
|
||||
class AtomProtocolHandler
|
||||
constructor: (resourcePath, safeMode) ->
|
||||
@loadPaths = []
|
||||
|
||||
unless safeMode
|
||||
@loadPaths.push(path.join(process.env.ATOM_HOME, 'dev', 'packages'))
|
||||
|
||||
@loadPaths.push(path.join(process.env.ATOM_HOME, 'packages'))
|
||||
@loadPaths.push(path.join(resourcePath, 'node_modules'))
|
||||
|
||||
@registerAtomProtocol()
|
||||
|
||||
# Creates the 'atom' custom protocol handler.
|
||||
registerAtomProtocol: ->
|
||||
protocol.registerFileProtocol 'atom', (request, callback) =>
|
||||
relativePath = path.normalize(request.url.substr(7))
|
||||
|
||||
if relativePath.indexOf('assets/') is 0
|
||||
assetsPath = path.join(process.env.ATOM_HOME, relativePath)
|
||||
filePath = assetsPath if fs.statSyncNoException(assetsPath).isFile?()
|
||||
|
||||
unless filePath
|
||||
for loadPath in @loadPaths
|
||||
filePath = path.join(loadPath, relativePath)
|
||||
break if fs.statSyncNoException(filePath).isFile?()
|
||||
|
||||
callback(filePath)
|
||||
@@ -1,222 +0,0 @@
|
||||
{BrowserWindow, app, dialog} = require 'electron'
|
||||
path = require 'path'
|
||||
fs = require 'fs'
|
||||
url = require 'url'
|
||||
_ = require 'underscore-plus'
|
||||
{EventEmitter} = require 'events'
|
||||
|
||||
module.exports =
|
||||
class AtomWindow
|
||||
_.extend @prototype, EventEmitter.prototype
|
||||
|
||||
@iconPath: path.resolve(__dirname, '..', '..', 'resources', 'atom.png')
|
||||
@includeShellLoadTime: true
|
||||
|
||||
browserWindow: null
|
||||
loaded: null
|
||||
isSpec: null
|
||||
|
||||
constructor: (settings={}) ->
|
||||
{@resourcePath, initialPaths, pathToOpen, locationsToOpen, @isSpec, @headless, @safeMode, @devMode} = settings
|
||||
locationsToOpen ?= [{pathToOpen}] if pathToOpen
|
||||
locationsToOpen ?= []
|
||||
|
||||
options =
|
||||
show: false
|
||||
title: 'Atom'
|
||||
'web-preferences':
|
||||
'direct-write': true
|
||||
|
||||
if @isSpec
|
||||
options['web-preferences']['page-visibility'] = true
|
||||
|
||||
# Don't set icon on Windows so the exe's ico will be used as window and
|
||||
# taskbar's icon. See https://github.com/atom/atom/issues/4811 for more.
|
||||
if process.platform is 'linux'
|
||||
options.icon = @constructor.iconPath
|
||||
|
||||
@browserWindow = new BrowserWindow options
|
||||
global.atomApplication.addWindow(this)
|
||||
|
||||
@handleEvents()
|
||||
|
||||
loadSettings = _.extend({}, settings)
|
||||
loadSettings.appVersion = app.getVersion()
|
||||
loadSettings.resourcePath = @resourcePath
|
||||
loadSettings.devMode ?= false
|
||||
loadSettings.safeMode ?= false
|
||||
loadSettings.atomHome = process.env.ATOM_HOME
|
||||
loadSettings.clearWindowState ?= false
|
||||
loadSettings.initialPaths ?=
|
||||
for {pathToOpen} in locationsToOpen when pathToOpen
|
||||
if fs.statSyncNoException(pathToOpen).isFile?()
|
||||
path.dirname(pathToOpen)
|
||||
else
|
||||
pathToOpen
|
||||
|
||||
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: (loadSettings) ->
|
||||
@browserWindow.loadURL url.format
|
||||
protocol: 'file'
|
||||
pathname: "#{@resourcePath}/static/index.html"
|
||||
slashes: true
|
||||
hash: encodeURIComponent(JSON.stringify(loadSettings))
|
||||
|
||||
getLoadSettings: ->
|
||||
if @browserWindow.webContents? and not @browserWindow.webContents.isLoading()
|
||||
hash = url.parse(@browserWindow.webContents.getURL()).hash.substr(1)
|
||||
JSON.parse(decodeURIComponent(hash))
|
||||
|
||||
hasProjectPath: -> @getLoadSettings().initialPaths?.length > 0
|
||||
|
||||
setupContextMenu: ->
|
||||
ContextMenu = require './context-menu'
|
||||
|
||||
@browserWindow.on 'context-menu', (menuTemplate) =>
|
||||
new ContextMenu(menuTemplate, this)
|
||||
|
||||
containsPaths: (paths) ->
|
||||
for pathToCheck in paths
|
||||
return false unless @containsPath(pathToCheck)
|
||||
true
|
||||
|
||||
containsPath: (pathToCheck) ->
|
||||
@getLoadSettings()?.initialPaths?.some (projectPath) ->
|
||||
if not projectPath
|
||||
false
|
||||
else if not pathToCheck
|
||||
false
|
||||
else if pathToCheck is projectPath
|
||||
true
|
||||
else if fs.statSyncNoException(pathToCheck).isDirectory?()
|
||||
false
|
||||
else if pathToCheck.indexOf(path.join(projectPath, path.sep)) is 0
|
||||
true
|
||||
else
|
||||
false
|
||||
|
||||
handleEvents: ->
|
||||
@browserWindow.on 'close', ->
|
||||
global.atomApplication.saveState(false)
|
||||
|
||||
@browserWindow.on 'closed', =>
|
||||
global.atomApplication.removeWindow(this)
|
||||
|
||||
@browserWindow.on 'unresponsive', =>
|
||||
return if @isSpec
|
||||
|
||||
chosen = dialog.showMessageBox @browserWindow,
|
||||
type: 'warning'
|
||||
buttons: ['Close', 'Keep Waiting']
|
||||
message: 'Editor is not responding'
|
||||
detail: 'The editor is not responding. Would you like to force close it or just keep waiting?'
|
||||
@browserWindow.destroy() if chosen is 0
|
||||
|
||||
@browserWindow.webContents.on 'crashed', =>
|
||||
global.atomApplication.exit(100) if @headless
|
||||
|
||||
chosen = dialog.showMessageBox @browserWindow,
|
||||
type: 'warning'
|
||||
buttons: ['Close Window', 'Reload', 'Keep It Open']
|
||||
message: 'The editor has crashed'
|
||||
detail: 'Please report this issue to https://github.com/atom/atom'
|
||||
switch chosen
|
||||
when 0 then @browserWindow.destroy()
|
||||
when 1 then @browserWindow.reload()
|
||||
|
||||
@browserWindow.webContents.on 'will-navigate', (event, url) =>
|
||||
unless url is @browserWindow.webContents.getURL()
|
||||
event.preventDefault()
|
||||
|
||||
@setupContextMenu()
|
||||
|
||||
if @isSpec
|
||||
# Workaround for https://github.com/atom/atom-shell/issues/380
|
||||
# Don't focus the window when it is being blurred during close or
|
||||
# else the app will crash on Windows.
|
||||
if process.platform is 'win32'
|
||||
@browserWindow.on 'close', => @isWindowClosing = true
|
||||
|
||||
# Spec window's web view should always have focus
|
||||
@browserWindow.on 'blur', =>
|
||||
@browserWindow.focusOnWebView() unless @isWindowClosing
|
||||
|
||||
openPath: (pathToOpen, initialLine, initialColumn) ->
|
||||
@openLocations([{pathToOpen, initialLine, initialColumn}])
|
||||
|
||||
openLocations: (locationsToOpen) ->
|
||||
if @loaded
|
||||
@sendMessage 'open-locations', locationsToOpen
|
||||
else
|
||||
@browserWindow.once 'window:loaded', => @openLocations(locationsToOpen)
|
||||
|
||||
sendMessage: (message, detail) ->
|
||||
@browserWindow.webContents.send 'message', message, detail
|
||||
|
||||
sendCommand: (command, args...) ->
|
||||
if @isSpecWindow()
|
||||
unless global.atomApplication.sendCommandToFirstResponder(command)
|
||||
switch command
|
||||
when 'window:reload' then @reload()
|
||||
when 'window:toggle-dev-tools' then @toggleDevTools()
|
||||
when 'window:close' then @close()
|
||||
else if @isWebViewFocused()
|
||||
@sendCommandToBrowserWindow(command, args...)
|
||||
else
|
||||
unless global.atomApplication.sendCommandToFirstResponder(command)
|
||||
@sendCommandToBrowserWindow(command, args...)
|
||||
|
||||
sendCommandToBrowserWindow: (command, args...) ->
|
||||
action = if args[0]?.contextCommand then 'context-command' else 'command'
|
||||
@browserWindow.webContents.send action, command, args...
|
||||
|
||||
getDimensions: ->
|
||||
[x, y] = @browserWindow.getPosition()
|
||||
[width, height] = @browserWindow.getSize()
|
||||
{x, y, width, height}
|
||||
|
||||
close: -> @browserWindow.close()
|
||||
|
||||
focus: -> @browserWindow.focus()
|
||||
|
||||
minimize: -> @browserWindow.minimize()
|
||||
|
||||
maximize: -> @browserWindow.maximize()
|
||||
|
||||
restore: -> @browserWindow.restore()
|
||||
|
||||
handlesAtomCommands: ->
|
||||
not @isSpecWindow() and @isWebViewFocused()
|
||||
|
||||
isFocused: -> @browserWindow.isFocused()
|
||||
|
||||
isMaximized: -> @browserWindow.isMaximized()
|
||||
|
||||
isMinimized: -> @browserWindow.isMinimized()
|
||||
|
||||
isWebViewFocused: -> @browserWindow.isWebViewFocused()
|
||||
|
||||
isSpecWindow: -> @isSpec
|
||||
|
||||
reload: -> @browserWindow.reload()
|
||||
|
||||
toggleDevTools: -> @browserWindow.toggleDevTools()
|
||||
@@ -1,140 +0,0 @@
|
||||
autoUpdater = null
|
||||
_ = require 'underscore-plus'
|
||||
Config = require '../config'
|
||||
{EventEmitter} = require 'events'
|
||||
path = require 'path'
|
||||
|
||||
IdleState = 'idle'
|
||||
CheckingState = 'checking'
|
||||
DownladingState = 'downloading'
|
||||
UpdateAvailableState = 'update-available'
|
||||
NoUpdateAvailableState = 'no-update-available'
|
||||
UnsupportedState = 'unsupported'
|
||||
ErrorState = 'error'
|
||||
|
||||
module.exports =
|
||||
class AutoUpdateManager
|
||||
_.extend @prototype, EventEmitter.prototype
|
||||
|
||||
constructor: (@version, @testMode, resourcePath) ->
|
||||
@state = IdleState
|
||||
@iconPath = path.resolve(__dirname, '..', '..', 'resources', 'atom.png')
|
||||
@feedUrl = "https://atom.io/api/updates?version=#{@version}"
|
||||
@config = new Config({configDirPath: process.env.ATOM_HOME, resourcePath, enablePersistence: true})
|
||||
@config.setSchema null, {type: 'object', properties: _.clone(require('../config-schema'))}
|
||||
@config.load()
|
||||
process.nextTick => @setupAutoUpdater()
|
||||
|
||||
setupAutoUpdater: ->
|
||||
if process.platform is 'win32'
|
||||
autoUpdater = require './auto-updater-win32'
|
||||
else
|
||||
{autoUpdater} = require 'electron'
|
||||
|
||||
autoUpdater.on 'error', (event, message) =>
|
||||
@setState(ErrorState)
|
||||
console.error "Error Downloading Update: #{message}"
|
||||
|
||||
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()
|
||||
|
||||
@config.onDidChange 'core.automaticallyUpdate', ({newValue}) =>
|
||||
if newValue
|
||||
@scheduleUpdateCheck()
|
||||
else
|
||||
@cancelScheduledUpdateCheck()
|
||||
|
||||
@scheduleUpdateCheck() if @config.get 'core.automaticallyUpdate'
|
||||
|
||||
switch process.platform
|
||||
when 'win32'
|
||||
@setState(UnsupportedState) unless autoUpdater.supportsUpdates()
|
||||
when 'linux'
|
||||
@setState(UnsupportedState)
|
||||
|
||||
emitUpdateAvailableEvent: ->
|
||||
return unless @releaseVersion?
|
||||
@emitWindowEvent('update-available', {@releaseVersion})
|
||||
return
|
||||
|
||||
emitWindowEvent: (eventName, payload) ->
|
||||
for atomWindow in @getWindows()
|
||||
atomWindow.sendMessage(eventName, payload)
|
||||
return
|
||||
|
||||
setState: (state) ->
|
||||
return if @state is state
|
||||
@state = state
|
||||
@emit 'state-changed', @state
|
||||
|
||||
getState: ->
|
||||
@state
|
||||
|
||||
scheduleUpdateCheck: ->
|
||||
# Only schedule update check periodically if running in release version and
|
||||
# and there is no existing scheduled update check.
|
||||
unless /\w{7}/.test(@version) or @checkForUpdatesIntervalID
|
||||
checkForUpdates = => @check(hidePopups: true)
|
||||
fourHours = 1000 * 60 * 60 * 4
|
||||
@checkForUpdatesIntervalID = setInterval(checkForUpdates, fourHours)
|
||||
checkForUpdates()
|
||||
|
||||
cancelScheduledUpdateCheck: ->
|
||||
if @checkForUpdatesIntervalID
|
||||
clearInterval(@checkForUpdatesIntervalID)
|
||||
@checkForUpdatesIntervalID = null
|
||||
|
||||
check: ({hidePopups}={}) ->
|
||||
unless hidePopups
|
||||
autoUpdater.once 'update-not-available', @onUpdateNotAvailable
|
||||
autoUpdater.once 'error', @onUpdateError
|
||||
|
||||
autoUpdater.checkForUpdates() unless @testMode
|
||||
|
||||
install: ->
|
||||
autoUpdater.quitAndInstall() unless @testMode
|
||||
|
||||
onUpdateNotAvailable: =>
|
||||
autoUpdater.removeListener 'error', @onUpdateError
|
||||
{dialog} = require 'electron'
|
||||
dialog.showMessageBox
|
||||
type: 'info'
|
||||
buttons: ['OK']
|
||||
icon: @iconPath
|
||||
message: 'No update available.'
|
||||
title: 'No Update Available'
|
||||
detail: "Version #{@version} is the latest version."
|
||||
|
||||
onUpdateError: (event, message) =>
|
||||
autoUpdater.removeListener 'update-not-available', @onUpdateNotAvailable
|
||||
{dialog} = require 'electron'
|
||||
dialog.showMessageBox
|
||||
type: 'warning'
|
||||
buttons: ['OK']
|
||||
icon: @iconPath
|
||||
message: 'There was an error checking for updates.'
|
||||
title: 'Update Error'
|
||||
detail: message
|
||||
|
||||
getWindows: ->
|
||||
global.atomApplication.windows
|
||||
@@ -1,63 +0,0 @@
|
||||
{EventEmitter} = require 'events'
|
||||
_ = require 'underscore-plus'
|
||||
SquirrelUpdate = require './squirrel-update'
|
||||
|
||||
class AutoUpdater
|
||||
_.extend @prototype, EventEmitter.prototype
|
||||
|
||||
setFeedURL: (@updateUrl) ->
|
||||
|
||||
quitAndInstall: ->
|
||||
if SquirrelUpdate.existsSync()
|
||||
SquirrelUpdate.restartAtom(require('electron').app)
|
||||
else
|
||||
require('electron').autoUpdater.quitAndInstall()
|
||||
|
||||
downloadUpdate: (callback) ->
|
||||
SquirrelUpdate.spawn ['--download', @updateUrl], (error, stdout) ->
|
||||
return callback(error) if error?
|
||||
|
||||
try
|
||||
# Last line of output is the JSON details about the releases
|
||||
json = stdout.trim().split('\n').pop()
|
||||
update = JSON.parse(json)?.releasesToApply?.pop?()
|
||||
catch error
|
||||
error.stdout = stdout
|
||||
return callback(error)
|
||||
|
||||
callback(null, update)
|
||||
|
||||
installUpdate: (callback) ->
|
||||
SquirrelUpdate.spawn(['--update', @updateUrl], callback)
|
||||
|
||||
supportsUpdates: ->
|
||||
SquirrelUpdate.existsSync()
|
||||
|
||||
checkForUpdates: ->
|
||||
throw new Error('Update URL is not set') unless @updateUrl
|
||||
|
||||
@emit 'checking-for-update'
|
||||
|
||||
unless SquirrelUpdate.existsSync()
|
||||
@emit 'update-not-available'
|
||||
return
|
||||
|
||||
@downloadUpdate (error, update) =>
|
||||
if error?
|
||||
@emit 'update-not-available'
|
||||
return
|
||||
|
||||
unless update?
|
||||
@emit 'update-not-available'
|
||||
return
|
||||
|
||||
@emit 'update-available'
|
||||
|
||||
@installUpdate (error) =>
|
||||
if error?
|
||||
@emit 'update-not-available'
|
||||
return
|
||||
|
||||
@emit 'update-downloaded', {}, update.releaseNotes, update.version, new Date(), 'https://atom.io', => @quitAndInstall()
|
||||
|
||||
module.exports = new AutoUpdater()
|
||||
@@ -1,24 +0,0 @@
|
||||
{Menu} = require 'electron'
|
||||
|
||||
module.exports =
|
||||
class ContextMenu
|
||||
constructor: (template, @atomWindow) ->
|
||||
template = @createClickHandlers(template)
|
||||
menu = Menu.buildFromTemplate(template)
|
||||
menu.popup(@atomWindow.browserWindow)
|
||||
|
||||
# It's necessary to build the event handlers in this process, otherwise
|
||||
# closures are dragged across processes and failed to be garbage collected
|
||||
# appropriately.
|
||||
createClickHandlers: (template) ->
|
||||
for item in template
|
||||
if item.command
|
||||
item.commandDetail ?= {}
|
||||
item.commandDetail.contextCommand = true
|
||||
item.commandDetail.atomWindow = @atomWindow
|
||||
do (item) =>
|
||||
item.click = =>
|
||||
global.atomApplication.sendCommandToWindow(item.command, @atomWindow, item.commandDetail)
|
||||
else if item.submenu
|
||||
@createClickHandlers(item.submenu)
|
||||
item
|
||||
@@ -1,191 +0,0 @@
|
||||
global.shellStartTime = Date.now()
|
||||
|
||||
process.on 'uncaughtException', (error={}) ->
|
||||
console.log(error.message) if error.message?
|
||||
console.log(error.stack) if error.stack?
|
||||
|
||||
{crashReporter, app} = require 'electron'
|
||||
fs = require 'fs-plus'
|
||||
path = require 'path'
|
||||
temp = require 'temp'
|
||||
yargs = require 'yargs'
|
||||
console.log = require 'nslog'
|
||||
|
||||
start = ->
|
||||
args = parseCommandLine()
|
||||
args.env = process.env
|
||||
setupAtomHome(args)
|
||||
setupCompileCache()
|
||||
return if handleStartupEventWithSquirrel()
|
||||
|
||||
# NB: This prevents Win10 from showing dupe items in the taskbar
|
||||
app.setAppUserModelId('com.squirrel.atom.atom')
|
||||
|
||||
addPathToOpen = (event, pathToOpen) ->
|
||||
event.preventDefault()
|
||||
args.pathsToOpen.push(pathToOpen)
|
||||
|
||||
addUrlToOpen = (event, urlToOpen) ->
|
||||
event.preventDefault()
|
||||
args.urlsToOpen.push(urlToOpen)
|
||||
|
||||
app.on 'open-file', addPathToOpen
|
||||
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
|
||||
|
||||
AtomApplication = require path.join(args.resourcePath, 'src', 'browser', 'atom-application')
|
||||
AtomApplication.open(args)
|
||||
|
||||
console.log("App load time: #{Date.now() - global.shellStartTime}ms") unless args.test
|
||||
|
||||
normalizeDriveLetterName = (filePath) ->
|
||||
if process.platform is 'win32'
|
||||
filePath.replace /^([a-z]):/, ([driveLetter]) -> driveLetter.toUpperCase() + ":"
|
||||
else
|
||||
filePath
|
||||
|
||||
handleStartupEventWithSquirrel = ->
|
||||
return false unless process.platform is 'win32'
|
||||
SquirrelUpdate = require './squirrel-update'
|
||||
squirrelCommand = process.argv[1]
|
||||
SquirrelUpdate.handleStartupEvent(app, squirrelCommand)
|
||||
|
||||
setupCrashReporter = ->
|
||||
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.getPath('home'), '.atom')
|
||||
AtomPortable = require './atom-portable'
|
||||
|
||||
if setPortable and not AtomPortable.isPortableInstall(process.platform, process.env.ATOM_HOME, atomHome)
|
||||
try
|
||||
AtomPortable.setPortable(atomHome)
|
||||
catch error
|
||||
console.log("Failed copying portable directory '#{atomHome}' to '#{AtomPortable.getPortableAtomHomePath()}'")
|
||||
console.log("#{error.message} #{error.stack}")
|
||||
|
||||
if AtomPortable.isPortableInstall(process.platform, process.env.ATOM_HOME, atomHome)
|
||||
atomHome = AtomPortable.getPortableAtomHomePath()
|
||||
|
||||
try
|
||||
atomHome = fs.realpathSync(atomHome)
|
||||
|
||||
process.env.ATOM_HOME = atomHome
|
||||
|
||||
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)
|
||||
options.usage """
|
||||
Atom Editor v#{version}
|
||||
|
||||
Usage: atom [options] [path ...]
|
||||
|
||||
One or more paths to files or folders may be specified. If there is an
|
||||
existing Atom window that contains all of the given folders, the paths
|
||||
will be opened in that window. Otherwise, they will be opened in a new
|
||||
window.
|
||||
|
||||
Environment Variables:
|
||||
|
||||
ATOM_DEV_RESOURCE_PATH The path from which Atom loads source code in dev mode.
|
||||
Defaults to `~/github/atom`.
|
||||
|
||||
ATOM_HOME The root path for all configuration files and folders.
|
||||
Defaults to `~/.atom`.
|
||||
"""
|
||||
# Deprecated 1.0 API preview flag
|
||||
options.alias('1', 'one').boolean('1').describe('1', 'This option is no longer supported.')
|
||||
options.boolean('include-deprecated-apis').describe('include-deprecated-apis', 'This option is not currently supported.')
|
||||
options.alias('d', 'dev').boolean('d').describe('d', 'Run in development mode.')
|
||||
options.alias('f', 'foreground').boolean('f').describe('f', 'Keep the browser process in the foreground.')
|
||||
options.alias('h', 'help').boolean('h').describe('h', 'Print this usage message.')
|
||||
options.alias('l', 'log-file').string('l').describe('l', 'Log all output to file.')
|
||||
options.alias('n', 'new-window').boolean('n').describe('n', 'Open a new window.')
|
||||
options.boolean('profile-startup').describe('profile-startup', 'Create a profile of the startup execution time.')
|
||||
options.alias('r', 'resource-path').string('r').describe('r', 'Set the path to the Atom source directory and enable dev-mode.')
|
||||
options.boolean('safe').describe('safe', 'Do not load packages from ~/.atom/packages or ~/.atom/dev/packages.')
|
||||
options.boolean('portable').describe('portable', 'Set portable mode. Copies the ~/.atom folder to be a sibling of the installed Atom location if a .atom folder is not already there.')
|
||||
options.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 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
|
||||
|
||||
if args.help
|
||||
process.stdout.write(options.help())
|
||||
process.exit(0)
|
||||
|
||||
if args.version
|
||||
writeFullVersion()
|
||||
process.exit(0)
|
||||
|
||||
addToLastWindow = args['add']
|
||||
executedFrom = args['executed-from']?.toString() ? process.cwd()
|
||||
devMode = args['dev']
|
||||
safeMode = args['safe']
|
||||
pathsToOpen = args._
|
||||
test = args['test']
|
||||
timeout = args['timeout']
|
||||
newWindow = args['new-window']
|
||||
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.getPath('home'), 'github', 'atom')
|
||||
setPortable = args.portable
|
||||
|
||||
if args['resource-path']
|
||||
devMode = true
|
||||
resourcePath = args['resource-path']
|
||||
|
||||
devMode = true if test
|
||||
resourcePath ?= devResourcePath if devMode
|
||||
|
||||
unless fs.statSyncNoException(resourcePath)
|
||||
resourcePath = path.dirname(path.dirname(__dirname))
|
||||
|
||||
# On Yosemite the $PATH is not inherited by the "open" command, so we have to
|
||||
# explicitly pass it by command line, see http://git.io/YC8_Ew.
|
||||
process.env.PATH = args['path-environment'] if args['path-environment']
|
||||
|
||||
resourcePath = normalizeDriveLetterName(resourcePath)
|
||||
devResourcePath = normalizeDriveLetterName(devResourcePath)
|
||||
|
||||
{resourcePath, devResourcePath, pathsToOpen, urlsToOpen, executedFrom, test,
|
||||
version, pidToKillWhenClosed, devMode, safeMode, newWindow,
|
||||
logFile, socketPath, userDataDir, profileStartup, timeout, setPortable,
|
||||
clearWindowState, addToLastWindow}
|
||||
|
||||
start()
|
||||
@@ -1,251 +0,0 @@
|
||||
ChildProcess = require 'child_process'
|
||||
fs = require 'fs-plus'
|
||||
path = require 'path'
|
||||
|
||||
appFolder = path.resolve(process.execPath, '..')
|
||||
rootAtomFolder = path.resolve(appFolder, '..')
|
||||
binFolder = path.join(rootAtomFolder, 'bin')
|
||||
updateDotExe = path.join(rootAtomFolder, 'Update.exe')
|
||||
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
|
||||
# and the output from standard out.
|
||||
spawn = (command, args, callback) ->
|
||||
stdout = ''
|
||||
|
||||
try
|
||||
spawnedProcess = ChildProcess.spawn(command, args)
|
||||
catch error
|
||||
# Spawn can throw an error
|
||||
process.nextTick -> callback?(error, stdout)
|
||||
return
|
||||
|
||||
spawnedProcess.stdout.on 'data', (data) -> stdout += data
|
||||
|
||||
error = null
|
||||
spawnedProcess.on 'error', (processError) -> error ?= processError
|
||||
spawnedProcess.on 'close', (code, signal) ->
|
||||
error ?= new Error("Command failed: #{signal ? code}") if code isnt 0
|
||||
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)
|
||||
|
||||
# Spawn the Update.exe with the given arguments and invoke the callback when
|
||||
# the command completes.
|
||||
spawnUpdate = (args, callback) ->
|
||||
spawn(updateDotExe, args, callback)
|
||||
|
||||
# Install the Open with Atom explorer context menu items via the registry.
|
||||
installContextMenu = (callback) ->
|
||||
addToRegistry = (args, callback) ->
|
||||
args.unshift('add')
|
||||
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, ->
|
||||
args = [keyPath, '/v', 'Icon', '/d', "\"#{process.execPath}\""]
|
||||
addToRegistry args, ->
|
||||
args = ["#{keyPath}\\command", '/ve', '/d', "\"#{process.execPath}\" \"#{arg}\""]
|
||||
addToRegistry(args, callback)
|
||||
|
||||
installMenu fileKeyPath, '%1', ->
|
||||
installMenu directoryKeyPath, '%1', ->
|
||||
installMenu backgroundKeyPath, '%V', ->
|
||||
installFileHandler(callback)
|
||||
|
||||
# Get the user's PATH environment variable registry value.
|
||||
getPath = (callback) ->
|
||||
spawnPowershell ['[environment]::GetEnvironmentVariable(\'Path\',\'User\')'], (error, stdout) ->
|
||||
if error?
|
||||
return callback(error)
|
||||
|
||||
pathOutput = stdout.replace(/^\s+|\s+$/g, '')
|
||||
callback(null, pathOutput)
|
||||
|
||||
# Uninstall the Open with Atom explorer context menu items via the registry.
|
||||
uninstallContextMenu = (callback) ->
|
||||
deleteFromRegistry = (keyPath, callback) ->
|
||||
spawnReg(['delete', keyPath, '/f'], callback)
|
||||
|
||||
deleteFromRegistry fileKeyPath, ->
|
||||
deleteFromRegistry directoryKeyPath, ->
|
||||
deleteFromRegistry backgroundKeyPath, ->
|
||||
deleteFromRegistry(applicationsKeyPath, callback)
|
||||
|
||||
# Add atom and apm to the PATH
|
||||
#
|
||||
# This is done by adding .cmd shims to the root bin folder in the Atom
|
||||
# install directory that point to the newly installed versions inside
|
||||
# the versioned app directories.
|
||||
addCommandsToPath = (callback) ->
|
||||
installCommands = (callback) ->
|
||||
atomCommandPath = path.join(binFolder, 'atom.cmd')
|
||||
relativeAtomPath = path.relative(binFolder, path.join(appFolder, 'resources', 'cli', 'atom.cmd'))
|
||||
atomCommand = "@echo off\r\n\"%~dp0\\#{relativeAtomPath}\" %*"
|
||||
|
||||
atomShCommandPath = path.join(binFolder, 'atom')
|
||||
relativeAtomShPath = path.relative(binFolder, path.join(appFolder, 'resources', 'cli', 'atom.sh'))
|
||||
atomShCommand = "#!/bin/sh\r\n\"$(dirname \"$0\")/#{relativeAtomShPath.replace(/\\/g, '/')}\" \"$@\"\r\necho"
|
||||
|
||||
apmCommandPath = path.join(binFolder, 'apm.cmd')
|
||||
relativeApmPath = path.relative(binFolder, path.join(process.resourcesPath, 'app', 'apm', 'bin', 'apm.cmd'))
|
||||
apmCommand = "@echo off\r\n\"%~dp0\\#{relativeApmPath}\" %*"
|
||||
|
||||
apmShCommandPath = path.join(binFolder, 'apm')
|
||||
relativeApmShPath = path.relative(binFolder, path.join(appFolder, 'resources', 'cli', 'apm.sh'))
|
||||
apmShCommand = "#!/bin/sh\r\n\"$(dirname \"$0\")/#{relativeApmShPath.replace(/\\/g, '/')}\" \"$@\""
|
||||
|
||||
fs.writeFile atomCommandPath, atomCommand, ->
|
||||
fs.writeFile atomShCommandPath, atomShCommand, ->
|
||||
fs.writeFile apmCommandPath, apmCommand, ->
|
||||
fs.writeFile apmShCommandPath, apmShCommand, ->
|
||||
callback()
|
||||
|
||||
addBinToPath = (pathSegments, callback) ->
|
||||
pathSegments.push(binFolder)
|
||||
newPathEnv = pathSegments.join(';')
|
||||
spawnSetx(['Path', newPathEnv], callback)
|
||||
|
||||
installCommands (error) ->
|
||||
return callback(error) if error?
|
||||
|
||||
getPath (error, pathEnv) ->
|
||||
return callback(error) if error?
|
||||
|
||||
pathSegments = pathEnv.split(/;+/).filter (pathSegment) -> pathSegment
|
||||
if pathSegments.indexOf(binFolder) is -1
|
||||
addBinToPath(pathSegments, callback)
|
||||
else
|
||||
callback()
|
||||
|
||||
# Remove atom and apm from the PATH
|
||||
removeCommandsFromPath = (callback) ->
|
||||
getPath (error, pathEnv) ->
|
||||
return callback(error) if error?
|
||||
|
||||
pathSegments = pathEnv.split(/;+/).filter (pathSegment) ->
|
||||
pathSegment and pathSegment isnt binFolder
|
||||
newPathEnv = pathSegments.join(';')
|
||||
|
||||
if pathEnv isnt newPathEnv
|
||||
spawnSetx(['Path', newPathEnv], callback)
|
||||
else
|
||||
callback()
|
||||
|
||||
# Create a desktop and start menu shortcut by using the command line API
|
||||
# provided by Squirrel's Update.exe
|
||||
createShortcuts = (callback) ->
|
||||
spawnUpdate(['--createShortcut', exeName], callback)
|
||||
|
||||
# Update the desktop and start menu shortcuts by using the command line API
|
||||
# provided by Squirrel's Update.exe
|
||||
updateShortcuts = (callback) ->
|
||||
if homeDirectory = fs.getHomeDirectory()
|
||||
desktopShortcutPath = path.join(homeDirectory, 'Desktop', 'Atom.lnk')
|
||||
# Check if the desktop shortcut has been previously deleted and
|
||||
# and keep it deleted if it was
|
||||
fs.exists desktopShortcutPath, (desktopShortcutExists) ->
|
||||
createShortcuts ->
|
||||
if desktopShortcutExists
|
||||
callback()
|
||||
else
|
||||
# Remove the unwanted desktop shortcut that was recreated
|
||||
fs.unlink(desktopShortcutPath, callback)
|
||||
else
|
||||
createShortcuts(callback)
|
||||
|
||||
# Remove the desktop and start menu shortcuts by using the command line API
|
||||
# provided by Squirrel's Update.exe
|
||||
removeShortcuts = (callback) ->
|
||||
spawnUpdate(['--removeShortcut', exeName], callback)
|
||||
|
||||
exports.spawn = spawnUpdate
|
||||
|
||||
# Is the Update.exe installed with Atom?
|
||||
exports.existsSync = ->
|
||||
fs.existsSync(updateDotExe)
|
||||
|
||||
# Restart Atom using the version pointed to by the atom.cmd shim
|
||||
exports.restartAtom = (app) ->
|
||||
if projectPath = global.atomApplication?.lastFocusedWindow?.projectPath
|
||||
args = [projectPath]
|
||||
app.once 'will-quit', -> spawn(path.join(binFolder, 'atom.cmd'), args)
|
||||
app.quit()
|
||||
|
||||
# Handle squirrel events denoted by --squirrel-* command line arguments.
|
||||
exports.handleStartupEvent = (app, squirrelCommand) ->
|
||||
switch squirrelCommand
|
||||
when '--squirrel-install'
|
||||
createShortcuts ->
|
||||
installContextMenu ->
|
||||
addCommandsToPath ->
|
||||
app.quit()
|
||||
true
|
||||
when '--squirrel-updated'
|
||||
updateShortcuts ->
|
||||
installContextMenu ->
|
||||
addCommandsToPath ->
|
||||
app.quit()
|
||||
true
|
||||
when '--squirrel-uninstall'
|
||||
removeShortcuts ->
|
||||
uninstallContextMenu ->
|
||||
removeCommandsFromPath ->
|
||||
app.quit()
|
||||
true
|
||||
when '--squirrel-obsolete'
|
||||
app.quit()
|
||||
true
|
||||
else
|
||||
false
|
||||
@@ -1,55 +0,0 @@
|
||||
BufferedProcess = require './buffered-process'
|
||||
path = require 'path'
|
||||
|
||||
# Extended: Like {BufferedProcess}, but accepts a Node script as the command
|
||||
# to run.
|
||||
#
|
||||
# This is necessary on Windows since it doesn't support shebang `#!` lines.
|
||||
#
|
||||
# ## Examples
|
||||
#
|
||||
# ```coffee
|
||||
# {BufferedNodeProcess} = require 'atom'
|
||||
# ```
|
||||
module.exports =
|
||||
class BufferedNodeProcess extends BufferedProcess
|
||||
|
||||
# Public: Runs the given Node script by spawning a new child process.
|
||||
#
|
||||
# * `options` An {Object} with the following keys:
|
||||
# * `command` The {String} path to the JavaScript script to execute.
|
||||
# * `args` The {Array} of arguments to pass to the script (optional).
|
||||
# * `options` The options {Object} to pass to Node's `ChildProcess.spawn`
|
||||
# method (optional).
|
||||
# * `stdout` The callback {Function} that receives a single argument which
|
||||
# contains the standard output from the command. The callback is
|
||||
# called as data is received but it's buffered to ensure only
|
||||
# complete lines are passed until the source stream closes. After
|
||||
# the source stream has closed all remaining data is sent in a
|
||||
# final call (optional).
|
||||
# * `stderr` The callback {Function} that receives a single argument which
|
||||
# contains the standard error output from the command. The
|
||||
# callback is called as data is received but it's buffered to
|
||||
# ensure only complete lines are passed until the source stream
|
||||
# closes. After the source stream has closed all remaining data
|
||||
# is sent in a final call (optional).
|
||||
# * `exit` The callback {Function} which receives a single argument
|
||||
# containing the exit status (optional).
|
||||
constructor: ({command, args, options, stdout, stderr, exit}) ->
|
||||
node =
|
||||
if process.platform is 'darwin'
|
||||
# Use a helper to prevent an icon from appearing on the Dock
|
||||
path.resolve(process.resourcesPath, '..', 'Frameworks',
|
||||
'Atom Helper.app', 'Contents', 'MacOS', 'Atom Helper')
|
||||
else
|
||||
process.execPath
|
||||
|
||||
options ?= {}
|
||||
options.env ?= Object.create(process.env)
|
||||
options.env['ELECTRON_RUN_AS_NODE'] = 1
|
||||
|
||||
args = args?.slice() ? []
|
||||
args.unshift(command)
|
||||
args.unshift('--no-deprecation')
|
||||
|
||||
super({command: node, args, options, stdout, stderr, exit})
|
||||
55
src/buffered-node-process.js
Normal file
55
src/buffered-node-process.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const BufferedProcess = require('./buffered-process')
|
||||
|
||||
// Extended: Like {BufferedProcess}, but accepts a Node script as the command
|
||||
// to run.
|
||||
//
|
||||
// This is necessary on Windows since it doesn't support shebang `#!` lines.
|
||||
//
|
||||
// ## Examples
|
||||
//
|
||||
// ```js
|
||||
// const {BufferedNodeProcess} = require('atom')
|
||||
// ```
|
||||
module.exports =
|
||||
class BufferedNodeProcess extends BufferedProcess {
|
||||
|
||||
// Public: Runs the given Node script by spawning a new child process.
|
||||
//
|
||||
// * `options` An {Object} with the following keys:
|
||||
// * `command` The {String} path to the JavaScript script to execute.
|
||||
// * `args` The {Array} of arguments to pass to the script (optional).
|
||||
// * `options` The options {Object} to pass to Node's `ChildProcess.spawn`
|
||||
// method (optional).
|
||||
// * `stdout` The callback {Function} that receives a single argument which
|
||||
// contains the standard output from the command. The callback is
|
||||
// called as data is received but it's buffered to ensure only
|
||||
// complete lines are passed until the source stream closes. After
|
||||
// the source stream has closed all remaining data is sent in a
|
||||
// final call (optional).
|
||||
// * `stderr` The callback {Function} that receives a single argument which
|
||||
// contains the standard error output from the command. The
|
||||
// callback is called as data is received but it's buffered to
|
||||
// ensure only complete lines are passed until the source stream
|
||||
// closes. After the source stream has closed all remaining data
|
||||
// is sent in a final call (optional).
|
||||
// * `exit` The callback {Function} which receives a single argument
|
||||
// containing the exit status (optional).
|
||||
constructor ({command, args, options = {}, stdout, stderr, exit}) {
|
||||
options.env = options.env || Object.create(process.env)
|
||||
options.env.ELECTRON_RUN_AS_NODE = 1
|
||||
options.env.ELECTRON_NO_ATTACH_CONSOLE = 1
|
||||
|
||||
args = args ? args.slice() : []
|
||||
args.unshift(command)
|
||||
args.unshift('--no-deprecation')
|
||||
|
||||
super({
|
||||
command: process.execPath,
|
||||
args,
|
||||
options,
|
||||
stdout,
|
||||
stderr,
|
||||
exit
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
_ = require 'underscore-plus'
|
||||
ChildProcess = require 'child_process'
|
||||
{Emitter} = require 'event-kit'
|
||||
path = require 'path'
|
||||
|
||||
# Extended: A wrapper which provides standard error/output line buffering for
|
||||
# Node's ChildProcess.
|
||||
#
|
||||
# ## Examples
|
||||
#
|
||||
# ```coffee
|
||||
# {BufferedProcess} = require 'atom'
|
||||
#
|
||||
# command = 'ps'
|
||||
# args = ['-ef']
|
||||
# stdout = (output) -> console.log(output)
|
||||
# exit = (code) -> console.log("ps -ef exited with #{code}")
|
||||
# process = new BufferedProcess({command, args, stdout, exit})
|
||||
# ```
|
||||
module.exports =
|
||||
class BufferedProcess
|
||||
###
|
||||
Section: Construction
|
||||
###
|
||||
|
||||
# Public: Runs the given command by spawning a new child process.
|
||||
#
|
||||
# * `options` An {Object} with the following keys:
|
||||
# * `command` The {String} command to execute.
|
||||
# * `args` The {Array} of arguments to pass to the command (optional).
|
||||
# * `options` {Object} (optional) The options {Object} to pass to Node's
|
||||
# `ChildProcess.spawn` method.
|
||||
# * `stdout` {Function} (optional) The callback that receives a single
|
||||
# argument which contains the standard output from the command. The
|
||||
# callback is called as data is received but it's buffered to ensure only
|
||||
# complete lines are passed until the source stream closes. After the
|
||||
# source stream has closed all remaining data is sent in a final call.
|
||||
# * `data` {String}
|
||||
# * `stderr` {Function} (optional) The callback that receives a single
|
||||
# argument which contains the standard error output from the command. The
|
||||
# callback is called as data is received but it's buffered to ensure only
|
||||
# complete lines are passed until the source stream closes. After the
|
||||
# source stream has closed all remaining data is sent in a final call.
|
||||
# * `data` {String}
|
||||
# * `exit` {Function} (optional) The callback which receives a single
|
||||
# argument containing the exit status.
|
||||
# * `code` {Number}
|
||||
constructor: ({command, args, options, stdout, stderr, exit}={}) ->
|
||||
@emitter = new Emitter
|
||||
options ?= {}
|
||||
@command = command
|
||||
# Related to joyent/node#2318
|
||||
if process.platform is 'win32'
|
||||
# Quote all arguments and escapes inner quotes
|
||||
if args?
|
||||
cmdArgs = args.filter (arg) -> arg?
|
||||
cmdArgs = cmdArgs.map (arg) =>
|
||||
if @isExplorerCommand(command) and /^\/[a-zA-Z]+,.*$/.test(arg)
|
||||
# Don't wrap /root,C:\folder style arguments to explorer calls in
|
||||
# quotes since they will not be interpreted correctly if they are
|
||||
arg
|
||||
else
|
||||
"\"#{arg.toString().replace(/"/g, '\\"')}\""
|
||||
else
|
||||
cmdArgs = []
|
||||
if /\s/.test(command)
|
||||
cmdArgs.unshift("\"#{command}\"")
|
||||
else
|
||||
cmdArgs.unshift(command)
|
||||
cmdArgs = ['/s', '/c', "\"#{cmdArgs.join(' ')}\""]
|
||||
cmdOptions = _.clone(options)
|
||||
cmdOptions.windowsVerbatimArguments = true
|
||||
@spawn(@getCmdPath(), cmdArgs, cmdOptions)
|
||||
else
|
||||
@spawn(command, args, options)
|
||||
|
||||
@killed = false
|
||||
@handleEvents(stdout, stderr, exit)
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
###
|
||||
|
||||
# Public: Will call your callback when an error will be raised by the process.
|
||||
# Usually this is due to the command not being available or not on the PATH.
|
||||
# You can call `handle()` on the object passed to your callback to indicate
|
||||
# that you have handled this error.
|
||||
#
|
||||
# * `callback` {Function} callback
|
||||
# * `errorObject` {Object}
|
||||
# * `error` {Object} the error object
|
||||
# * `handle` {Function} call this to indicate you have handled the error.
|
||||
# The error will not be thrown if this function is called.
|
||||
#
|
||||
# Returns a {Disposable}
|
||||
onWillThrowError: (callback) ->
|
||||
@emitter.on 'will-throw-error', callback
|
||||
|
||||
###
|
||||
Section: Helper Methods
|
||||
###
|
||||
|
||||
# Helper method to pass data line by line.
|
||||
#
|
||||
# * `stream` The Stream to read from.
|
||||
# * `onLines` The callback to call with each line of data.
|
||||
# * `onDone` The callback to call when the stream has closed.
|
||||
bufferStream: (stream, onLines, onDone) ->
|
||||
stream.setEncoding('utf8')
|
||||
buffered = ''
|
||||
|
||||
stream.on 'data', (data) =>
|
||||
return if @killed
|
||||
buffered += data
|
||||
lastNewlineIndex = buffered.lastIndexOf('\n')
|
||||
if lastNewlineIndex isnt -1
|
||||
onLines(buffered.substring(0, lastNewlineIndex + 1))
|
||||
buffered = buffered.substring(lastNewlineIndex + 1)
|
||||
|
||||
stream.on 'close', =>
|
||||
return if @killed
|
||||
onLines(buffered) if buffered.length > 0
|
||||
onDone()
|
||||
|
||||
# Kill all child processes of the spawned cmd.exe process on Windows.
|
||||
#
|
||||
# This is required since killing the cmd.exe does not terminate child
|
||||
# processes.
|
||||
killOnWindows: ->
|
||||
return unless @process?
|
||||
|
||||
parentPid = @process.pid
|
||||
cmd = 'wmic'
|
||||
args = [
|
||||
'process'
|
||||
'where'
|
||||
"(ParentProcessId=#{parentPid})"
|
||||
'get'
|
||||
'processid'
|
||||
]
|
||||
|
||||
try
|
||||
wmicProcess = ChildProcess.spawn(cmd, args)
|
||||
catch spawnError
|
||||
@killProcess()
|
||||
return
|
||||
|
||||
wmicProcess.on 'error', -> # ignore errors
|
||||
output = ''
|
||||
wmicProcess.stdout.on 'data', (data) -> output += data
|
||||
wmicProcess.stdout.on 'close', =>
|
||||
pidsToKill = output.split(/\s+/)
|
||||
.filter (pid) -> /^\d+$/.test(pid)
|
||||
.map (pid) -> parseInt(pid)
|
||||
.filter (pid) -> pid isnt parentPid and 0 < pid < Infinity
|
||||
|
||||
for pid in pidsToKill
|
||||
try
|
||||
process.kill(pid)
|
||||
@killProcess()
|
||||
|
||||
killProcess: ->
|
||||
@process?.kill()
|
||||
@process = null
|
||||
|
||||
isExplorerCommand: (command) ->
|
||||
if command is 'explorer.exe' or command is 'explorer'
|
||||
true
|
||||
else if process.env.SystemRoot
|
||||
command is path.join(process.env.SystemRoot, 'explorer.exe') or command is path.join(process.env.SystemRoot, 'explorer')
|
||||
else
|
||||
false
|
||||
|
||||
getCmdPath: ->
|
||||
if process.env.comspec
|
||||
process.env.comspec
|
||||
else if process.env.SystemRoot
|
||||
path.join(process.env.SystemRoot, 'System32', 'cmd.exe')
|
||||
else
|
||||
'cmd.exe'
|
||||
|
||||
# Public: Terminate the process.
|
||||
kill: ->
|
||||
return if @killed
|
||||
|
||||
@killed = true
|
||||
if process.platform is 'win32'
|
||||
@killOnWindows()
|
||||
else
|
||||
@killProcess()
|
||||
|
||||
undefined
|
||||
|
||||
spawn: (command, args, options) ->
|
||||
try
|
||||
@process = ChildProcess.spawn(command, args, options)
|
||||
catch spawnError
|
||||
process.nextTick => @handleError(spawnError)
|
||||
|
||||
handleEvents: (stdout, stderr, exit) ->
|
||||
return unless @process?
|
||||
|
||||
stdoutClosed = true
|
||||
stderrClosed = true
|
||||
processExited = true
|
||||
exitCode = 0
|
||||
triggerExitCallback = ->
|
||||
return if @killed
|
||||
if stdoutClosed and stderrClosed and processExited
|
||||
exit?(exitCode)
|
||||
|
||||
if stdout
|
||||
stdoutClosed = false
|
||||
@bufferStream @process.stdout, stdout, ->
|
||||
stdoutClosed = true
|
||||
triggerExitCallback()
|
||||
|
||||
if stderr
|
||||
stderrClosed = false
|
||||
@bufferStream @process.stderr, stderr, ->
|
||||
stderrClosed = true
|
||||
triggerExitCallback()
|
||||
|
||||
if exit
|
||||
processExited = false
|
||||
@process.on 'exit', (code) ->
|
||||
exitCode = code
|
||||
processExited = true
|
||||
triggerExitCallback()
|
||||
|
||||
@process.on 'error', (error) => @handleError(error)
|
||||
return
|
||||
|
||||
handleError: (error) ->
|
||||
handled = false
|
||||
handle = -> handled = true
|
||||
|
||||
@emitter.emit 'will-throw-error', {error, handle}
|
||||
|
||||
if error.code is 'ENOENT' and error.syscall.indexOf('spawn') is 0
|
||||
error = new Error("Failed to spawn command `#{@command}`. Make sure `#{@command}` is installed and on your PATH", error.path)
|
||||
error.name = 'BufferedProcessError'
|
||||
|
||||
throw error unless handled
|
||||
313
src/buffered-process.js
Normal file
313
src/buffered-process.js
Normal file
@@ -0,0 +1,313 @@
|
||||
const _ = require('underscore-plus')
|
||||
const ChildProcess = require('child_process')
|
||||
const {Emitter} = require('event-kit')
|
||||
const path = require('path')
|
||||
|
||||
// Extended: A wrapper which provides standard error/output line buffering for
|
||||
// Node's ChildProcess.
|
||||
//
|
||||
// ## Examples
|
||||
//
|
||||
// ```js
|
||||
// {BufferedProcess} = require('atom')
|
||||
//
|
||||
// const command = 'ps'
|
||||
// const args = ['-ef']
|
||||
// const stdout = (output) => console.log(output)
|
||||
// const exit = (code) => console.log("ps -ef exited with #{code}")
|
||||
// const process = new BufferedProcess({command, args, stdout, exit})
|
||||
// ```
|
||||
module.exports =
|
||||
class BufferedProcess {
|
||||
/*
|
||||
Section: Construction
|
||||
*/
|
||||
|
||||
// Public: Runs the given command by spawning a new child process.
|
||||
//
|
||||
// * `options` An {Object} with the following keys:
|
||||
// * `command` The {String} command to execute.
|
||||
// * `args` The {Array} of arguments to pass to the command (optional).
|
||||
// * `options` {Object} (optional) The options {Object} to pass to Node's
|
||||
// `ChildProcess.spawn` method.
|
||||
// * `stdout` {Function} (optional) The callback that receives a single
|
||||
// argument which contains the standard output from the command. The
|
||||
// callback is called as data is received but it's buffered to ensure only
|
||||
// complete lines are passed until the source stream closes. After the
|
||||
// source stream has closed all remaining data is sent in a final call.
|
||||
// * `data` {String}
|
||||
// * `stderr` {Function} (optional) The callback that receives a single
|
||||
// argument which contains the standard error output from the command. The
|
||||
// callback is called as data is received but it's buffered to ensure only
|
||||
// complete lines are passed until the source stream closes. After the
|
||||
// source stream has closed all remaining data is sent in a final call.
|
||||
// * `data` {String}
|
||||
// * `exit` {Function} (optional) The callback which receives a single
|
||||
// argument containing the exit status.
|
||||
// * `code` {Number}
|
||||
// * `autoStart` {Boolean} (optional) Whether the command will automatically start
|
||||
// when this BufferedProcess is created. Defaults to true. When set to false you
|
||||
// must call the `start` method to start the process.
|
||||
constructor ({command, args, options = {}, stdout, stderr, exit, autoStart = true} = {}) {
|
||||
this.emitter = new Emitter()
|
||||
this.command = command
|
||||
this.args = args
|
||||
this.options = options
|
||||
this.stdout = stdout
|
||||
this.stderr = stderr
|
||||
this.exit = exit
|
||||
if (autoStart === true) {
|
||||
this.start()
|
||||
}
|
||||
this.killed = false
|
||||
}
|
||||
|
||||
start () {
|
||||
if (this.started === true) return
|
||||
|
||||
this.started = true
|
||||
// Related to joyent/node#2318
|
||||
if (process.platform === 'win32' && this.options.shell === undefined) {
|
||||
this.spawnWithEscapedWindowsArgs(this.command, this.args, this.options)
|
||||
} else {
|
||||
this.spawn(this.command, this.args, this.options)
|
||||
}
|
||||
this.handleEvents(this.stdout, this.stderr, this.exit)
|
||||
}
|
||||
|
||||
// Windows has a bunch of special rules that node still doesn't take care of for you
|
||||
spawnWithEscapedWindowsArgs (command, args, options) {
|
||||
let cmdArgs = []
|
||||
// Quote all arguments and escapes inner quotes
|
||||
if (args) {
|
||||
cmdArgs = args.filter((arg) => arg != null)
|
||||
.map((arg) => {
|
||||
if (this.isExplorerCommand(command) && /^\/[a-zA-Z]+,.*$/.test(arg)) {
|
||||
// Don't wrap /root,C:\folder style arguments to explorer calls in
|
||||
// quotes since they will not be interpreted correctly if they are
|
||||
return arg
|
||||
} else {
|
||||
// Escape double quotes by putting a backslash in front of them
|
||||
return `\"${arg.toString().replace(/"/g, '\\"')}\"`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// The command itself is quoted if it contains spaces, &, ^, | or # chars
|
||||
cmdArgs.unshift(/\s|&|\^|\(|\)|\||#/.test(command) ? `\"${command}\"` : command)
|
||||
|
||||
const cmdOptions = _.clone(options)
|
||||
cmdOptions.windowsVerbatimArguments = true
|
||||
|
||||
this.spawn(this.getCmdPath(), ['/s', '/d', '/c', `\"${cmdArgs.join(' ')}\"`], cmdOptions)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Event Subscription
|
||||
*/
|
||||
|
||||
// Public: Will call your callback when an error will be raised by the process.
|
||||
// Usually this is due to the command not being available or not on the PATH.
|
||||
// You can call `handle()` on the object passed to your callback to indicate
|
||||
// that you have handled this error.
|
||||
//
|
||||
// * `callback` {Function} callback
|
||||
// * `errorObject` {Object}
|
||||
// * `error` {Object} the error object
|
||||
// * `handle` {Function} call this to indicate you have handled the error.
|
||||
// The error will not be thrown if this function is called.
|
||||
//
|
||||
// Returns a {Disposable}
|
||||
onWillThrowError (callback) {
|
||||
return this.emitter.on('will-throw-error', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Helper Methods
|
||||
*/
|
||||
|
||||
// Helper method to pass data line by line.
|
||||
//
|
||||
// * `stream` The Stream to read from.
|
||||
// * `onLines` The callback to call with each line of data.
|
||||
// * `onDone` The callback to call when the stream has closed.
|
||||
bufferStream (stream, onLines, onDone) {
|
||||
stream.setEncoding('utf8')
|
||||
let buffered = ''
|
||||
|
||||
stream.on('data', (data) => {
|
||||
if (this.killed) return
|
||||
|
||||
let bufferedLength = buffered.length
|
||||
buffered += data
|
||||
let lastNewlineIndex = data.lastIndexOf('\n')
|
||||
|
||||
if (lastNewlineIndex !== -1) {
|
||||
let lineLength = lastNewlineIndex + bufferedLength + 1
|
||||
onLines(buffered.substring(0, lineLength))
|
||||
buffered = buffered.substring(lineLength)
|
||||
}
|
||||
})
|
||||
|
||||
stream.on('close', () => {
|
||||
if (this.killed) return
|
||||
if (buffered.length > 0) onLines(buffered)
|
||||
onDone()
|
||||
})
|
||||
}
|
||||
|
||||
// Kill all child processes of the spawned cmd.exe process on Windows.
|
||||
//
|
||||
// This is required since killing the cmd.exe does not terminate child
|
||||
// processes.
|
||||
killOnWindows () {
|
||||
if (!this.process) return
|
||||
|
||||
const parentPid = this.process.pid
|
||||
const cmd = 'wmic'
|
||||
const args = [
|
||||
'process',
|
||||
'where',
|
||||
`(ParentProcessId=${parentPid})`,
|
||||
'get',
|
||||
'processid'
|
||||
]
|
||||
|
||||
let wmicProcess
|
||||
|
||||
try {
|
||||
wmicProcess = ChildProcess.spawn(cmd, args)
|
||||
} catch (spawnError) {
|
||||
this.killProcess()
|
||||
return
|
||||
}
|
||||
|
||||
wmicProcess.on('error', () => {}) // ignore errors
|
||||
|
||||
let output = ''
|
||||
wmicProcess.stdout.on('data', (data) => {
|
||||
output += data
|
||||
})
|
||||
wmicProcess.stdout.on('close', () => {
|
||||
for (let pid of output.split(/\s+/)) {
|
||||
if (!/^\d{1,10}$/.test(pid)) continue
|
||||
pid = parseInt(pid, 10)
|
||||
|
||||
if (!pid || pid === parentPid) continue
|
||||
|
||||
try {
|
||||
process.kill(pid)
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
this.killProcess()
|
||||
})
|
||||
}
|
||||
|
||||
killProcess () {
|
||||
if (this.process) this.process.kill()
|
||||
this.process = null
|
||||
}
|
||||
|
||||
isExplorerCommand (command) {
|
||||
if (command === 'explorer.exe' || command === 'explorer') {
|
||||
return true
|
||||
} else if (process.env.SystemRoot) {
|
||||
return command === path.join(process.env.SystemRoot, 'explorer.exe') || command === path.join(process.env.SystemRoot, 'explorer')
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
getCmdPath () {
|
||||
if (process.env.comspec) {
|
||||
return process.env.comspec
|
||||
} else if (process.env.SystemRoot) {
|
||||
return path.join(process.env.SystemRoot, 'System32', 'cmd.exe')
|
||||
} else {
|
||||
return 'cmd.exe'
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Terminate the process.
|
||||
kill () {
|
||||
if (this.killed) return
|
||||
|
||||
this.killed = true
|
||||
if (process.platform === 'win32') {
|
||||
this.killOnWindows()
|
||||
} else {
|
||||
this.killProcess()
|
||||
}
|
||||
}
|
||||
|
||||
spawn (command, args, options) {
|
||||
try {
|
||||
this.process = ChildProcess.spawn(command, args, options)
|
||||
} catch (spawnError) {
|
||||
process.nextTick(() => this.handleError(spawnError))
|
||||
}
|
||||
}
|
||||
|
||||
handleEvents (stdout, stderr, exit) {
|
||||
if (!this.process) return
|
||||
|
||||
const triggerExitCallback = () => {
|
||||
if (this.killed) return
|
||||
if (stdoutClosed && stderrClosed && processExited && typeof exit === 'function') {
|
||||
exit(exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
let stdoutClosed = true
|
||||
let stderrClosed = true
|
||||
let processExited = true
|
||||
let exitCode = 0
|
||||
|
||||
if (stdout) {
|
||||
stdoutClosed = false
|
||||
this.bufferStream(this.process.stdout, stdout, () => {
|
||||
stdoutClosed = true
|
||||
triggerExitCallback()
|
||||
})
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
stderrClosed = false
|
||||
this.bufferStream(this.process.stderr, stderr, () => {
|
||||
stderrClosed = true
|
||||
triggerExitCallback()
|
||||
})
|
||||
}
|
||||
|
||||
if (exit) {
|
||||
processExited = false
|
||||
this.process.on('exit', (code) => {
|
||||
exitCode = code
|
||||
processExited = true
|
||||
triggerExitCallback()
|
||||
})
|
||||
}
|
||||
|
||||
this.process.on('error', (error) => {
|
||||
this.handleError(error)
|
||||
})
|
||||
}
|
||||
|
||||
handleError (error) {
|
||||
let handled = false
|
||||
|
||||
const handle = () => {
|
||||
handled = true
|
||||
}
|
||||
|
||||
this.emitter.emit('will-throw-error', {error, handle})
|
||||
|
||||
if (error.code === 'ENOENT' && error.syscall.indexOf('spawn') === 0) {
|
||||
error = new Error(`Failed to spawn command \`${this.command}\`. Make sure \`${this.command}\` is installed and on your PATH`, error.path)
|
||||
error.name = 'BufferedProcessError'
|
||||
}
|
||||
|
||||
if (!handled) throw error
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
crypto = require 'crypto'
|
||||
clipboard = require './safe-clipboard'
|
||||
|
||||
# Extended: Represents the clipboard used for copying and pasting in Atom.
|
||||
#
|
||||
# An instance of this class is always available as the `atom.clipboard` global.
|
||||
#
|
||||
# ## Examples
|
||||
#
|
||||
# ```coffee
|
||||
# atom.clipboard.write('hello')
|
||||
#
|
||||
# console.log(atom.clipboard.read()) # 'hello'
|
||||
# ```
|
||||
module.exports =
|
||||
class Clipboard
|
||||
constructor: ->
|
||||
@reset()
|
||||
|
||||
reset: ->
|
||||
@metadata = null
|
||||
@signatureForMetadata = null
|
||||
|
||||
# Creates an `md5` hash of some text.
|
||||
#
|
||||
# * `text` A {String} to hash.
|
||||
#
|
||||
# Returns a hashed {String}.
|
||||
md5: (text) ->
|
||||
crypto.createHash('md5').update(text, 'utf8').digest('hex')
|
||||
|
||||
# Public: Write the given text to the clipboard.
|
||||
#
|
||||
# The metadata associated with the text is available by calling
|
||||
# {::readWithMetadata}.
|
||||
#
|
||||
# * `text` The {String} to store.
|
||||
# * `metadata` (optional) The additional info to associate with the text.
|
||||
write: (text, metadata) ->
|
||||
@signatureForMetadata = @md5(text)
|
||||
@metadata = metadata
|
||||
clipboard.writeText(text)
|
||||
|
||||
# Public: Read the text from the clipboard.
|
||||
#
|
||||
# Returns a {String}.
|
||||
read: ->
|
||||
clipboard.readText()
|
||||
|
||||
# Public: Read the text from the clipboard and return both the text and the
|
||||
# associated metadata.
|
||||
#
|
||||
# Returns an {Object} with the following keys:
|
||||
# * `text` The {String} clipboard text.
|
||||
# * `metadata` The metadata stored by an earlier call to {::write}.
|
||||
readWithMetadata: ->
|
||||
text = @read()
|
||||
if @signatureForMetadata is @md5(text)
|
||||
{text, @metadata}
|
||||
else
|
||||
{text}
|
||||
69
src/clipboard.js
Normal file
69
src/clipboard.js
Normal file
@@ -0,0 +1,69 @@
|
||||
const crypto = require('crypto')
|
||||
const {clipboard} = require('electron')
|
||||
|
||||
// Extended: Represents the clipboard used for copying and pasting in Atom.
|
||||
//
|
||||
// An instance of this class is always available as the `atom.clipboard` global.
|
||||
//
|
||||
// ## Examples
|
||||
//
|
||||
// ```js
|
||||
// atom.clipboard.write('hello')
|
||||
//
|
||||
// console.log(atom.clipboard.read()) // 'hello'
|
||||
// ```
|
||||
module.exports =
|
||||
class Clipboard {
|
||||
constructor () {
|
||||
this.reset()
|
||||
}
|
||||
|
||||
reset () {
|
||||
this.metadata = null
|
||||
this.signatureForMetadata = null
|
||||
}
|
||||
|
||||
// Creates an `md5` hash of some text.
|
||||
//
|
||||
// * `text` A {String} to hash.
|
||||
//
|
||||
// Returns a hashed {String}.
|
||||
md5 (text) {
|
||||
return crypto.createHash('md5').update(text, 'utf8').digest('hex')
|
||||
}
|
||||
|
||||
// Public: Write the given text to the clipboard.
|
||||
//
|
||||
// The metadata associated with the text is available by calling
|
||||
// {::readWithMetadata}.
|
||||
//
|
||||
// * `text` The {String} to store.
|
||||
// * `metadata` (optional) The additional info to associate with the text.
|
||||
write (text, metadata) {
|
||||
this.signatureForMetadata = this.md5(text)
|
||||
this.metadata = metadata
|
||||
clipboard.writeText(text)
|
||||
}
|
||||
|
||||
// Public: Read the text from the clipboard.
|
||||
//
|
||||
// Returns a {String}.
|
||||
read () {
|
||||
return clipboard.readText()
|
||||
}
|
||||
|
||||
// Public: Read the text from the clipboard and return both the text and the
|
||||
// associated metadata.
|
||||
//
|
||||
// Returns an {Object} with the following keys:
|
||||
// * `text` The {String} clipboard text.
|
||||
// * `metadata` The metadata stored by an earlier call to {::write}.
|
||||
readWithMetadata () {
|
||||
const text = this.read()
|
||||
if (this.signatureForMetadata === this.md5(text)) {
|
||||
return {text, metadata: this.metadata}
|
||||
} else {
|
||||
return {text}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,13 +36,10 @@ exports.compile = function (sourceCode, filePath) {
|
||||
var output = CoffeeScript.compile(sourceCode, {
|
||||
filename: filePath,
|
||||
sourceFiles: [filePath],
|
||||
sourceMap: true
|
||||
inlineMap: true
|
||||
})
|
||||
|
||||
var js = output.js
|
||||
js += '\n'
|
||||
js += '//# sourceMappingURL=data:application/json;base64,'
|
||||
js += new Buffer(output.v3SourceMap).toString('base64')
|
||||
js += '\n'
|
||||
return js
|
||||
// Strip sourceURL from output so there wouldn't be duplicate entries
|
||||
// in devtools.
|
||||
return output.replace(/\/\/# sourceURL=[^'"\n]+\s*$/, '')
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
_ = require 'underscore-plus'
|
||||
ParsedColor = null
|
||||
|
||||
# Essential: A simple color class returned from {Config::get} when the value
|
||||
# at the key path is of type 'color'.
|
||||
module.exports =
|
||||
class Color
|
||||
# Essential: Parse a {String} or {Object} into a {Color}.
|
||||
#
|
||||
# * `value` A {String} such as `'white'`, `#ff00ff`, or
|
||||
# `'rgba(255, 15, 60, .75)'` or an {Object} with `red`, `green`, `blue`,
|
||||
# and `alpha` properties.
|
||||
#
|
||||
# Returns a {Color} or `null` if it cannot be parsed.
|
||||
@parse: (value) ->
|
||||
return null if _.isArray(value) or _.isFunction(value)
|
||||
return null unless _.isObject(value) or _.isString(value)
|
||||
|
||||
ParsedColor ?= require 'color'
|
||||
|
||||
try
|
||||
parsedColor = new ParsedColor(value)
|
||||
catch error
|
||||
return null
|
||||
|
||||
new Color(parsedColor.red(), parsedColor.green(), parsedColor.blue(), parsedColor.alpha())
|
||||
|
||||
constructor: (red, green, blue, alpha) ->
|
||||
Object.defineProperties this,
|
||||
red:
|
||||
set: (newRed) -> red = parseColor(newRed)
|
||||
get: -> red
|
||||
enumerable: true
|
||||
configurable: false
|
||||
green:
|
||||
set: (newGreen) -> green = parseColor(newGreen)
|
||||
get: -> green
|
||||
enumerable: true
|
||||
configurable: false
|
||||
blue:
|
||||
set: (newBlue) -> blue = parseColor(newBlue)
|
||||
get: -> blue
|
||||
enumerable: true
|
||||
configurable: false
|
||||
alpha:
|
||||
set: (newAlpha) -> alpha = parseAlpha(newAlpha)
|
||||
get: -> alpha
|
||||
enumerable: true
|
||||
configurable: false
|
||||
|
||||
@red = red
|
||||
@green = green
|
||||
@blue = blue
|
||||
@alpha = alpha
|
||||
|
||||
# Essential: Returns a {String} in the form `'#abcdef'`.
|
||||
toHexString: ->
|
||||
"##{numberToHexString(@red)}#{numberToHexString(@green)}#{numberToHexString(@blue)}"
|
||||
|
||||
# Essential: Returns a {String} in the form `'rgba(25, 50, 75, .9)'`.
|
||||
toRGBAString: ->
|
||||
"rgba(#{@red}, #{@green}, #{@blue}, #{@alpha})"
|
||||
|
||||
isEqual: (color) ->
|
||||
return true if this is color
|
||||
color = Color.parse(color) unless color instanceof Color
|
||||
return false unless color?
|
||||
color.red is @red and color.blue is @blue and color.green is @green and color.alpha is @alpha
|
||||
|
||||
clone: -> new Color(@red, @green, @blue, @alpha)
|
||||
|
||||
parseColor = (color) ->
|
||||
color = parseInt(color)
|
||||
color = 0 if isNaN(color)
|
||||
color = Math.max(color, 0)
|
||||
color = Math.min(color, 255)
|
||||
color
|
||||
|
||||
parseAlpha = (alpha) ->
|
||||
alpha = parseFloat(alpha)
|
||||
alpha = 1 if isNaN(alpha)
|
||||
alpha = Math.max(alpha, 0)
|
||||
alpha = Math.min(alpha, 1)
|
||||
alpha
|
||||
|
||||
numberToHexString = (number) ->
|
||||
hex = number.toString(16)
|
||||
hex = "0#{hex}" if number < 16
|
||||
hex
|
||||
129
src/color.js
Normal file
129
src/color.js
Normal file
@@ -0,0 +1,129 @@
|
||||
let ParsedColor = null
|
||||
|
||||
// Essential: A simple color class returned from {Config::get} when the value
|
||||
// at the key path is of type 'color'.
|
||||
module.exports =
|
||||
class Color {
|
||||
// Essential: Parse a {String} or {Object} into a {Color}.
|
||||
//
|
||||
// * `value` A {String} such as `'white'`, `#ff00ff`, or
|
||||
// `'rgba(255, 15, 60, .75)'` or an {Object} with `red`, `green`, `blue`,
|
||||
// and `alpha` properties.
|
||||
//
|
||||
// Returns a {Color} or `null` if it cannot be parsed.
|
||||
static parse (value) {
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
break
|
||||
case 'object':
|
||||
if (Array.isArray(value)) { return null }
|
||||
break
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
if (!ParsedColor) {
|
||||
ParsedColor = require('color')
|
||||
}
|
||||
|
||||
try {
|
||||
var parsedColor = new ParsedColor(value)
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
|
||||
return new Color(parsedColor.red(), parsedColor.green(), parsedColor.blue(), parsedColor.alpha())
|
||||
}
|
||||
|
||||
constructor (red, green, blue, alpha) {
|
||||
this.red = red
|
||||
this.green = green
|
||||
this.blue = blue
|
||||
this.alpha = alpha
|
||||
}
|
||||
|
||||
set red (red) {
|
||||
this._red = parseColor(red)
|
||||
}
|
||||
|
||||
set green (green) {
|
||||
this._green = parseColor(green)
|
||||
}
|
||||
|
||||
set blue (blue) {
|
||||
this._blue = parseColor(blue)
|
||||
}
|
||||
|
||||
set alpha (alpha) {
|
||||
this._alpha = parseAlpha(alpha)
|
||||
}
|
||||
|
||||
get red () {
|
||||
return this._red
|
||||
}
|
||||
|
||||
get green () {
|
||||
return this._green
|
||||
}
|
||||
|
||||
get blue () {
|
||||
return this._blue
|
||||
}
|
||||
|
||||
get alpha () {
|
||||
return this._alpha
|
||||
}
|
||||
|
||||
// Essential: Returns a {String} in the form `'#abcdef'`.
|
||||
toHexString () {
|
||||
return `#${numberToHexString(this.red)}${numberToHexString(this.green)}${numberToHexString(this.blue)}`
|
||||
}
|
||||
|
||||
// Essential: Returns a {String} in the form `'rgba(25, 50, 75, .9)'`.
|
||||
toRGBAString () {
|
||||
return `rgba(${this.red}, ${this.green}, ${this.blue}, ${this.alpha})`
|
||||
}
|
||||
|
||||
toJSON () {
|
||||
return this.alpha === 1 ? this.toHexString() : this.toRGBAString()
|
||||
}
|
||||
|
||||
toString () {
|
||||
return this.toRGBAString()
|
||||
}
|
||||
|
||||
isEqual (color) {
|
||||
if (this === color) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!(color instanceof Color)) {
|
||||
color = Color.parse(color)
|
||||
}
|
||||
|
||||
if (color == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
return color.red === this.red && color.blue === this.blue && color.green === this.green && color.alpha === this.alpha
|
||||
}
|
||||
|
||||
clone () {
|
||||
return new Color(this.red, this.green, this.blue, this.alpha)
|
||||
}
|
||||
}
|
||||
|
||||
function parseColor (colorString) {
|
||||
const color = parseInt(colorString, 10)
|
||||
return isNaN(color) ? 0 : Math.min(Math.max(color, 0), 255)
|
||||
}
|
||||
|
||||
function parseAlpha (alphaString) {
|
||||
const alpha = parseFloat(alphaString)
|
||||
return isNaN(alpha) ? 1 : Math.min(Math.max(alpha, 0), 1)
|
||||
}
|
||||
|
||||
function numberToHexString (number) {
|
||||
const hex = number.toString(16)
|
||||
return number < 16 ? `0${hex}` : hex
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
path = require 'path'
|
||||
fs = require 'fs-plus'
|
||||
runas = null # defer until used
|
||||
|
||||
symlinkCommand = (sourcePath, destinationPath, callback) ->
|
||||
fs.unlink destinationPath, (error) ->
|
||||
if error? and error?.code isnt 'ENOENT'
|
||||
callback(error)
|
||||
else
|
||||
fs.makeTree path.dirname(destinationPath), (error) ->
|
||||
if error?
|
||||
callback(error)
|
||||
else
|
||||
fs.symlink sourcePath, destinationPath, callback
|
||||
|
||||
symlinkCommandWithPrivilegeSync = (sourcePath, destinationPath) ->
|
||||
runas ?= require 'runas'
|
||||
if runas('/bin/rm', ['-f', destinationPath], admin: true) isnt 0
|
||||
throw new Error("Failed to remove '#{destinationPath}'")
|
||||
|
||||
if runas('/bin/mkdir', ['-p', path.dirname(destinationPath)], admin: true) isnt 0
|
||||
throw new Error("Failed to create directory '#{destinationPath}'")
|
||||
|
||||
if runas('/bin/ln', ['-s', sourcePath, destinationPath], admin: true) isnt 0
|
||||
throw new Error("Failed to symlink '#{sourcePath}' to '#{destinationPath}'")
|
||||
|
||||
module.exports =
|
||||
class CommandInstaller
|
||||
constructor: (@appVersion, @applicationDelegate) ->
|
||||
|
||||
getInstallDirectory: ->
|
||||
"/usr/local/bin"
|
||||
|
||||
getResourcesDirectory: ->
|
||||
process.resourcesPath
|
||||
|
||||
installShellCommandsInteractively: ->
|
||||
showErrorDialog = (error) =>
|
||||
@applicationDelegate.confirm
|
||||
message: "Failed to install shell commands"
|
||||
detailedMessage: error.message
|
||||
|
||||
@installAtomCommand true, (error) =>
|
||||
if error?
|
||||
showErrorDialog(error)
|
||||
else
|
||||
@installApmCommand true, (error) =>
|
||||
if error?
|
||||
showErrorDialog(error)
|
||||
else
|
||||
@applicationDelegate.confirm
|
||||
message: "Commands installed."
|
||||
detailedMessage: "The shell commands `atom` and `apm` are installed."
|
||||
|
||||
installAtomCommand: (askForPrivilege, callback) ->
|
||||
programName = if @appVersion.includes("beta")
|
||||
"atom-beta"
|
||||
else
|
||||
"atom"
|
||||
|
||||
commandPath = path.join(@getResourcesDirectory(), 'app', 'atom.sh')
|
||||
@createSymlink commandPath, programName, askForPrivilege, callback
|
||||
|
||||
installApmCommand: (askForPrivilege, callback) ->
|
||||
programName = if @appVersion.includes("beta")
|
||||
"apm-beta"
|
||||
else
|
||||
"apm"
|
||||
|
||||
commandPath = path.join(@getResourcesDirectory(), 'app', 'apm', 'node_modules', '.bin', 'apm')
|
||||
@createSymlink commandPath, programName, askForPrivilege, callback
|
||||
|
||||
createSymlink: (commandPath, commandName, askForPrivilege, callback) ->
|
||||
return unless process.platform is 'darwin'
|
||||
|
||||
destinationPath = path.join(@getInstallDirectory(), commandName)
|
||||
|
||||
fs.readlink destinationPath, (error, realpath) ->
|
||||
if realpath is commandPath
|
||||
callback()
|
||||
return
|
||||
|
||||
symlinkCommand commandPath, destinationPath, (error) ->
|
||||
if askForPrivilege and error?.code is 'EACCES'
|
||||
try
|
||||
error = null
|
||||
symlinkCommandWithPrivilegeSync(commandPath, destinationPath)
|
||||
catch err
|
||||
error = err
|
||||
|
||||
callback?(error)
|
||||
102
src/command-installer.js
Normal file
102
src/command-installer.js
Normal file
@@ -0,0 +1,102 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs-plus')
|
||||
|
||||
module.exports =
|
||||
class CommandInstaller {
|
||||
constructor (applicationDelegate) {
|
||||
this.applicationDelegate = applicationDelegate
|
||||
}
|
||||
|
||||
initialize (appVersion) {
|
||||
this.appVersion = appVersion
|
||||
}
|
||||
|
||||
getInstallDirectory () {
|
||||
return '/usr/local/bin'
|
||||
}
|
||||
|
||||
getResourcesDirectory () {
|
||||
return process.resourcesPath
|
||||
}
|
||||
|
||||
installShellCommandsInteractively () {
|
||||
const showErrorDialog = (error) => {
|
||||
this.applicationDelegate.confirm({
|
||||
message: 'Failed to install shell commands',
|
||||
detail: error.message
|
||||
}, () => {})
|
||||
}
|
||||
|
||||
this.installAtomCommand(true, (error, atomCommandName) => {
|
||||
if (error) return showErrorDialog(error)
|
||||
this.installApmCommand(true, (error, apmCommandName) => {
|
||||
if (error) return showErrorDialog(error)
|
||||
this.applicationDelegate.confirm({
|
||||
message: 'Commands installed.',
|
||||
detail: `The shell commands \`${atomCommandName}\` and \`${apmCommandName}\` are installed.`
|
||||
}, () => {})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
getCommandNameForChannel (commandName) {
|
||||
let channelMatch = this.appVersion.match(/beta|nightly/)
|
||||
let channel = channelMatch ? channelMatch[0] : ''
|
||||
|
||||
switch (channel) {
|
||||
case 'beta':
|
||||
return `${commandName}-beta`
|
||||
case 'nightly':
|
||||
return `${commandName}-nightly`
|
||||
default:
|
||||
return commandName
|
||||
}
|
||||
}
|
||||
|
||||
installAtomCommand (askForPrivilege, callback) {
|
||||
this.installCommand(
|
||||
path.join(this.getResourcesDirectory(), 'app', 'atom.sh'),
|
||||
this.getCommandNameForChannel('atom'),
|
||||
askForPrivilege,
|
||||
callback
|
||||
)
|
||||
}
|
||||
|
||||
installApmCommand (askForPrivilege, callback) {
|
||||
this.installCommand(
|
||||
path.join(this.getResourcesDirectory(), 'app', 'apm', 'node_modules', '.bin', 'apm'),
|
||||
this.getCommandNameForChannel('apm'),
|
||||
askForPrivilege,
|
||||
callback
|
||||
)
|
||||
}
|
||||
|
||||
installCommand (commandPath, commandName, askForPrivilege, callback) {
|
||||
if (process.platform !== 'darwin') return callback()
|
||||
|
||||
const destinationPath = path.join(this.getInstallDirectory(), commandName)
|
||||
|
||||
fs.readlink(destinationPath, (error, realpath) => {
|
||||
if (error && error.code !== 'ENOENT') return callback(error)
|
||||
if (realpath === commandPath) return callback(null, commandName)
|
||||
this.createSymlink(fs, commandPath, destinationPath, error => {
|
||||
if (error && error.code === 'EACCES' && askForPrivilege) {
|
||||
const fsAdmin = require('fs-admin')
|
||||
this.createSymlink(fsAdmin, commandPath, destinationPath, (error) => { callback(error, commandName) })
|
||||
} else {
|
||||
callback(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
createSymlink (fs, sourcePath, destinationPath, callback) {
|
||||
fs.unlink(destinationPath, (error) => {
|
||||
if (error && error.code !== 'ENOENT') return callback(error)
|
||||
fs.makeTree(path.dirname(destinationPath), (error) => {
|
||||
if (error) return callback(error)
|
||||
fs.symlink(sourcePath, destinationPath, callback)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
|
||||
{calculateSpecificity, validateSelector} = require 'clear-cut'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
SequenceCount = 0
|
||||
|
||||
# Public: Associates listener functions with commands in a
|
||||
# context-sensitive way using CSS selectors. You can access a global instance of
|
||||
# this class via `atom.commands`, and commands registered there will be
|
||||
# presented in the command palette.
|
||||
#
|
||||
# The global command registry facilitates a style of event handling known as
|
||||
# *event delegation* that was popularized by jQuery. Atom commands are expressed
|
||||
# as custom DOM events that can be invoked on the currently focused element via
|
||||
# a key binding or manually via the command palette. Rather than binding
|
||||
# listeners for command events directly to DOM nodes, you instead register
|
||||
# command event listeners globally on `atom.commands` and constrain them to
|
||||
# specific kinds of elements with CSS selectors.
|
||||
#
|
||||
# Command names must follow the `namespace:action` pattern, where `namespace`
|
||||
# will typically be the name of your package, and `action` describes the
|
||||
# behavior of your command. If either part consists of multiple words, these
|
||||
# must be separated by hyphens. E.g. `awesome-package:turn-it-up-to-eleven`.
|
||||
# All words should be lowercased.
|
||||
#
|
||||
# As the event bubbles upward through the DOM, all registered event listeners
|
||||
# with matching selectors are invoked in order of specificity. In the event of a
|
||||
# specificity tie, the most recently registered listener is invoked first. This
|
||||
# mirrors the "cascade" semantics of CSS. Event listeners are invoked in the
|
||||
# context of the current DOM node, meaning `this` always points at
|
||||
# `event.currentTarget`. As is normally the case with DOM events,
|
||||
# `stopPropagation` and `stopImmediatePropagation` can be used to terminate the
|
||||
# bubbling process and prevent invocation of additional listeners.
|
||||
#
|
||||
# ## Example
|
||||
#
|
||||
# Here is a command that inserts the current date in an editor:
|
||||
#
|
||||
# ```coffee
|
||||
# atom.commands.add 'atom-text-editor',
|
||||
# 'user:insert-date': (event) ->
|
||||
# editor = @getModel()
|
||||
# editor.insertText(new Date().toLocaleString())
|
||||
# ```
|
||||
module.exports =
|
||||
class CommandRegistry
|
||||
constructor: ->
|
||||
@rootNode = null
|
||||
@clear()
|
||||
|
||||
clear: ->
|
||||
@registeredCommands = {}
|
||||
@selectorBasedListenersByCommandName = {}
|
||||
@inlineListenersByCommandName = {}
|
||||
@emitter = new Emitter
|
||||
|
||||
attach: (@rootNode) ->
|
||||
@commandRegistered(command) for command of @selectorBasedListenersByCommandName
|
||||
@commandRegistered(command) for command of @inlineListenersByCommandName
|
||||
|
||||
destroy: ->
|
||||
for commandName of @registeredCommands
|
||||
@rootNode.removeEventListener(commandName, @handleCommandEvent, true)
|
||||
return
|
||||
|
||||
# Public: Add one or more command listeners associated with a selector.
|
||||
#
|
||||
# ## Arguments: Registering One Command
|
||||
#
|
||||
# * `target` A {String} containing a CSS selector or a DOM element. If you
|
||||
# pass a selector, the command will be globally associated with all matching
|
||||
# elements. The `,` combinator is not currently supported. If you pass a
|
||||
# DOM element, the command will be associated with just that element.
|
||||
# * `commandName` A {String} containing the name of a command you want to
|
||||
# handle such as `user:insert-date`.
|
||||
# * `callback` A {Function} to call when the given command is invoked on an
|
||||
# element matching the selector. It will be called with `this` referencing
|
||||
# the matching DOM node.
|
||||
# * `event` A standard DOM event instance. Call `stopPropagation` or
|
||||
# `stopImmediatePropagation` to terminate bubbling early.
|
||||
#
|
||||
# ## Arguments: Registering Multiple Commands
|
||||
#
|
||||
# * `target` A {String} containing a CSS selector or a DOM element. If you
|
||||
# pass a selector, the commands will be globally associated with all
|
||||
# matching elements. The `,` combinator is not currently supported.
|
||||
# If you pass a DOM element, the command will be associated with just that
|
||||
# element.
|
||||
# * `commands` An {Object} mapping command names like `user:insert-date` to
|
||||
# listener {Function}s.
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to remove the
|
||||
# added command handler(s).
|
||||
add: (target, commandName, callback) ->
|
||||
if typeof commandName is 'object'
|
||||
commands = commandName
|
||||
disposable = new CompositeDisposable
|
||||
for commandName, callback of commands
|
||||
disposable.add @add(target, commandName, callback)
|
||||
return disposable
|
||||
|
||||
if typeof callback isnt 'function'
|
||||
throw new Error("Can't register a command with non-function callback.")
|
||||
|
||||
if typeof target is 'string'
|
||||
validateSelector(target)
|
||||
@addSelectorBasedListener(target, commandName, callback)
|
||||
else
|
||||
@addInlineListener(target, commandName, callback)
|
||||
|
||||
addSelectorBasedListener: (selector, commandName, callback) ->
|
||||
@selectorBasedListenersByCommandName[commandName] ?= []
|
||||
listenersForCommand = @selectorBasedListenersByCommandName[commandName]
|
||||
listener = new SelectorBasedListener(selector, callback)
|
||||
listenersForCommand.push(listener)
|
||||
|
||||
@commandRegistered(commandName)
|
||||
|
||||
new Disposable =>
|
||||
listenersForCommand.splice(listenersForCommand.indexOf(listener), 1)
|
||||
delete @selectorBasedListenersByCommandName[commandName] if listenersForCommand.length is 0
|
||||
|
||||
addInlineListener: (element, commandName, callback) ->
|
||||
@inlineListenersByCommandName[commandName] ?= new WeakMap
|
||||
|
||||
listenersForCommand = @inlineListenersByCommandName[commandName]
|
||||
unless listenersForElement = listenersForCommand.get(element)
|
||||
listenersForElement = []
|
||||
listenersForCommand.set(element, listenersForElement)
|
||||
listener = new InlineListener(callback)
|
||||
listenersForElement.push(listener)
|
||||
|
||||
@commandRegistered(commandName)
|
||||
|
||||
new Disposable ->
|
||||
listenersForElement.splice(listenersForElement.indexOf(listener), 1)
|
||||
listenersForCommand.delete(element) if listenersForElement.length is 0
|
||||
|
||||
# Public: Find all registered commands matching a query.
|
||||
#
|
||||
# * `params` An {Object} containing one or more of the following keys:
|
||||
# * `target` A DOM node that is the hypothetical target of a given command.
|
||||
#
|
||||
# Returns an {Array} of {Object}s containing the following keys:
|
||||
# * `name` The name of the command. For example, `user:insert-date`.
|
||||
# * `displayName` The display name of the command. For example,
|
||||
# `User: Insert Date`.
|
||||
findCommands: ({target}) ->
|
||||
commandNames = new Set
|
||||
commands = []
|
||||
currentTarget = target
|
||||
loop
|
||||
for name, listeners of @inlineListenersByCommandName
|
||||
if listeners.has(currentTarget) and not commandNames.has(name)
|
||||
commandNames.add(name)
|
||||
commands.push({name, displayName: _.humanizeEventName(name)})
|
||||
|
||||
for commandName, listeners of @selectorBasedListenersByCommandName
|
||||
for listener in listeners
|
||||
if currentTarget.webkitMatchesSelector?(listener.selector)
|
||||
unless commandNames.has(commandName)
|
||||
commandNames.add(commandName)
|
||||
commands.push
|
||||
name: commandName
|
||||
displayName: _.humanizeEventName(commandName)
|
||||
|
||||
break if currentTarget is window
|
||||
currentTarget = currentTarget.parentNode ? window
|
||||
|
||||
commands
|
||||
|
||||
# Public: Simulate the dispatch of a command on a DOM node.
|
||||
#
|
||||
# This can be useful for testing when you want to simulate the invocation of a
|
||||
# command on a detached DOM node. Otherwise, the DOM node in question needs to
|
||||
# be attached to the document so the event bubbles up to the root node to be
|
||||
# processed.
|
||||
#
|
||||
# * `target` The DOM node at which to start bubbling the command event.
|
||||
# * `commandName` {String} indicating the name of the command to dispatch.
|
||||
dispatch: (target, commandName, detail) ->
|
||||
event = new CustomEvent(commandName, {bubbles: true, detail})
|
||||
Object.defineProperty(event, 'target', value: target)
|
||||
@handleCommandEvent(event)
|
||||
|
||||
# Public: Invoke the given callback before dispatching a command event.
|
||||
#
|
||||
# * `callback` {Function} to be called before dispatching each command
|
||||
# * `event` The Event that will be dispatched
|
||||
onWillDispatch: (callback) ->
|
||||
@emitter.on 'will-dispatch', callback
|
||||
|
||||
# Public: Invoke the given callback after dispatching a command event.
|
||||
#
|
||||
# * `callback` {Function} to be called after dispatching each command
|
||||
# * `event` The Event that was dispatched
|
||||
onDidDispatch: (callback) ->
|
||||
@emitter.on 'did-dispatch', callback
|
||||
|
||||
getSnapshot: ->
|
||||
snapshot = {}
|
||||
for commandName, listeners of @selectorBasedListenersByCommandName
|
||||
snapshot[commandName] = listeners.slice()
|
||||
snapshot
|
||||
|
||||
restoreSnapshot: (snapshot) ->
|
||||
@selectorBasedListenersByCommandName = {}
|
||||
for commandName, listeners of snapshot
|
||||
@selectorBasedListenersByCommandName[commandName] = listeners.slice()
|
||||
return
|
||||
|
||||
handleCommandEvent: (event) =>
|
||||
propagationStopped = false
|
||||
immediatePropagationStopped = false
|
||||
matched = false
|
||||
currentTarget = event.target
|
||||
{preventDefault, stopPropagation, stopImmediatePropagation, abortKeyBinding} = event
|
||||
|
||||
dispatchedEvent = new CustomEvent(event.type, {bubbles: true, detail: event.detail})
|
||||
Object.defineProperty dispatchedEvent, 'eventPhase', value: Event.BUBBLING_PHASE
|
||||
Object.defineProperty dispatchedEvent, 'currentTarget', get: -> currentTarget
|
||||
Object.defineProperty dispatchedEvent, 'target', value: currentTarget
|
||||
Object.defineProperty dispatchedEvent, 'preventDefault', value: ->
|
||||
event.preventDefault()
|
||||
Object.defineProperty dispatchedEvent, 'stopPropagation', value: ->
|
||||
event.stopPropagation()
|
||||
propagationStopped = true
|
||||
Object.defineProperty dispatchedEvent, 'stopImmediatePropagation', value: ->
|
||||
event.stopImmediatePropagation()
|
||||
propagationStopped = true
|
||||
immediatePropagationStopped = true
|
||||
Object.defineProperty dispatchedEvent, 'abortKeyBinding', value: ->
|
||||
event.abortKeyBinding?()
|
||||
|
||||
for key in Object.keys(event)
|
||||
dispatchedEvent[key] = event[key]
|
||||
|
||||
@emitter.emit 'will-dispatch', dispatchedEvent
|
||||
|
||||
loop
|
||||
listeners = @inlineListenersByCommandName[event.type]?.get(currentTarget) ? []
|
||||
if currentTarget.webkitMatchesSelector?
|
||||
selectorBasedListeners =
|
||||
(@selectorBasedListenersByCommandName[event.type] ? [])
|
||||
.filter (listener) -> currentTarget.webkitMatchesSelector(listener.selector)
|
||||
.sort (a, b) -> a.compare(b)
|
||||
listeners = listeners.concat(selectorBasedListeners)
|
||||
|
||||
matched = true if listeners.length > 0
|
||||
|
||||
for listener in listeners
|
||||
break if immediatePropagationStopped
|
||||
listener.callback.call(currentTarget, dispatchedEvent)
|
||||
|
||||
break if currentTarget is window
|
||||
break if propagationStopped
|
||||
currentTarget = currentTarget.parentNode ? window
|
||||
|
||||
@emitter.emit 'did-dispatch', dispatchedEvent
|
||||
|
||||
matched
|
||||
|
||||
commandRegistered: (commandName) ->
|
||||
if @rootNode? and not @registeredCommands[commandName]
|
||||
@rootNode.addEventListener(commandName, @handleCommandEvent, true)
|
||||
@registeredCommands[commandName] = true
|
||||
|
||||
class SelectorBasedListener
|
||||
constructor: (@selector, @callback) ->
|
||||
@specificity = calculateSpecificity(@selector)
|
||||
@sequenceNumber = SequenceCount++
|
||||
|
||||
compare: (other) ->
|
||||
other.specificity - @specificity or
|
||||
other.sequenceNumber - @sequenceNumber
|
||||
|
||||
class InlineListener
|
||||
constructor: (@callback) ->
|
||||
457
src/command-registry.js
Normal file
457
src/command-registry.js
Normal file
@@ -0,0 +1,457 @@
|
||||
'use strict'
|
||||
|
||||
const { Emitter, Disposable, CompositeDisposable } = require('event-kit')
|
||||
const { calculateSpecificity, validateSelector } = require('clear-cut')
|
||||
const _ = require('underscore-plus')
|
||||
|
||||
let SequenceCount = 0
|
||||
|
||||
// Public: Associates listener functions with commands in a
|
||||
// context-sensitive way using CSS selectors. You can access a global instance of
|
||||
// this class via `atom.commands`, and commands registered there will be
|
||||
// presented in the command palette.
|
||||
//
|
||||
// The global command registry facilitates a style of event handling known as
|
||||
// *event delegation* that was popularized by jQuery. Atom commands are expressed
|
||||
// as custom DOM events that can be invoked on the currently focused element via
|
||||
// a key binding or manually via the command palette. Rather than binding
|
||||
// listeners for command events directly to DOM nodes, you instead register
|
||||
// command event listeners globally on `atom.commands` and constrain them to
|
||||
// specific kinds of elements with CSS selectors.
|
||||
//
|
||||
// Command names must follow the `namespace:action` pattern, where `namespace`
|
||||
// will typically be the name of your package, and `action` describes the
|
||||
// behavior of your command. If either part consists of multiple words, these
|
||||
// must be separated by hyphens. E.g. `awesome-package:turn-it-up-to-eleven`.
|
||||
// All words should be lowercased.
|
||||
//
|
||||
// As the event bubbles upward through the DOM, all registered event listeners
|
||||
// with matching selectors are invoked in order of specificity. In the event of a
|
||||
// specificity tie, the most recently registered listener is invoked first. This
|
||||
// mirrors the "cascade" semantics of CSS. Event listeners are invoked in the
|
||||
// context of the current DOM node, meaning `this` always points at
|
||||
// `event.currentTarget`. As is normally the case with DOM events,
|
||||
// `stopPropagation` and `stopImmediatePropagation` can be used to terminate the
|
||||
// bubbling process and prevent invocation of additional listeners.
|
||||
//
|
||||
// ## Example
|
||||
//
|
||||
// Here is a command that inserts the current date in an editor:
|
||||
//
|
||||
// ```coffee
|
||||
// atom.commands.add 'atom-text-editor',
|
||||
// 'user:insert-date': (event) ->
|
||||
// editor = @getModel()
|
||||
// editor.insertText(new Date().toLocaleString())
|
||||
// ```
|
||||
module.exports = class CommandRegistry {
|
||||
constructor () {
|
||||
this.handleCommandEvent = this.handleCommandEvent.bind(this)
|
||||
this.rootNode = null
|
||||
this.clear()
|
||||
}
|
||||
|
||||
clear () {
|
||||
this.registeredCommands = {}
|
||||
this.selectorBasedListenersByCommandName = {}
|
||||
this.inlineListenersByCommandName = {}
|
||||
this.emitter = new Emitter()
|
||||
}
|
||||
|
||||
attach (rootNode) {
|
||||
this.rootNode = rootNode
|
||||
for (const command in this.selectorBasedListenersByCommandName) {
|
||||
this.commandRegistered(command)
|
||||
}
|
||||
|
||||
for (const command in this.inlineListenersByCommandName) {
|
||||
this.commandRegistered(command)
|
||||
}
|
||||
}
|
||||
|
||||
destroy () {
|
||||
for (const commandName in this.registeredCommands) {
|
||||
this.rootNode.removeEventListener(
|
||||
commandName,
|
||||
this.handleCommandEvent,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Add one or more command listeners associated with a selector.
|
||||
//
|
||||
// ## Arguments: Registering One Command
|
||||
//
|
||||
// * `target` A {String} containing a CSS selector or a DOM element. If you
|
||||
// pass a selector, the command will be globally associated with all matching
|
||||
// elements. The `,` combinator is not currently supported. If you pass a
|
||||
// DOM element, the command will be associated with just that element.
|
||||
// * `commandName` A {String} containing the name of a command you want to
|
||||
// handle such as `user:insert-date`.
|
||||
// * `listener` A listener which handles the event. Either a {Function} to
|
||||
// call when the given command is invoked on an element matching the
|
||||
// selector, or an {Object} with a `didDispatch` property which is such a
|
||||
// function.
|
||||
//
|
||||
// The function (`listener` itself if it is a function, or the `didDispatch`
|
||||
// method if `listener` is an object) will be called with `this` referencing
|
||||
// the matching DOM node and the following argument:
|
||||
// * `event`: A standard DOM event instance. Call `stopPropagation` or
|
||||
// `stopImmediatePropagation` to terminate bubbling early.
|
||||
//
|
||||
// Additionally, `listener` may have additional properties which are returned
|
||||
// to those who query using `atom.commands.findCommands`, as well as several
|
||||
// meaningful metadata properties:
|
||||
// * `displayName`: Overrides any generated `displayName` that would
|
||||
// otherwise be generated from the event name.
|
||||
// * `description`: Used by consumers to display detailed information about
|
||||
// the command.
|
||||
// * `hiddenInCommandPalette`: If `true`, this command will not appear in
|
||||
// the bundled command palette by default, but can still be shown with.
|
||||
// the `Command Palette: Show Hidden Commands` command. This is a good
|
||||
// option when you need to register large numbers of commands that don't
|
||||
// make sense to be executed from the command palette. Please use this
|
||||
// option conservatively, as it could reduce the discoverability of your
|
||||
// package's commands.
|
||||
//
|
||||
// ## Arguments: Registering Multiple Commands
|
||||
//
|
||||
// * `target` A {String} containing a CSS selector or a DOM element. If you
|
||||
// pass a selector, the commands will be globally associated with all
|
||||
// matching elements. The `,` combinator is not currently supported.
|
||||
// If you pass a DOM element, the command will be associated with just that
|
||||
// element.
|
||||
// * `commands` An {Object} mapping command names like `user:insert-date` to
|
||||
// listener {Function}s.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to remove the
|
||||
// added command handler(s).
|
||||
add (target, commandName, listener, throwOnInvalidSelector = true) {
|
||||
if (typeof commandName === 'object') {
|
||||
const commands = commandName
|
||||
throwOnInvalidSelector = listener
|
||||
const disposable = new CompositeDisposable()
|
||||
for (commandName in commands) {
|
||||
listener = commands[commandName]
|
||||
disposable.add(this.add(target, commandName, listener, throwOnInvalidSelector))
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
|
||||
if (listener == null) {
|
||||
throw new Error('Cannot register a command with a null listener.')
|
||||
}
|
||||
|
||||
// type Listener = ((e: CustomEvent) => void) | {
|
||||
// displayName?: string,
|
||||
// description?: string,
|
||||
// didDispatch(e: CustomEvent): void,
|
||||
// }
|
||||
if ((typeof listener !== 'function') && (typeof listener.didDispatch !== 'function')) {
|
||||
throw new Error('Listener must be a callback function or an object with a didDispatch method.')
|
||||
}
|
||||
|
||||
if (typeof target === 'string') {
|
||||
if (throwOnInvalidSelector) {
|
||||
validateSelector(target)
|
||||
}
|
||||
return this.addSelectorBasedListener(target, commandName, listener)
|
||||
} else {
|
||||
return this.addInlineListener(target, commandName, listener)
|
||||
}
|
||||
}
|
||||
|
||||
addSelectorBasedListener (selector, commandName, listener) {
|
||||
if (this.selectorBasedListenersByCommandName[commandName] == null) {
|
||||
this.selectorBasedListenersByCommandName[commandName] = []
|
||||
}
|
||||
const listenersForCommand = this.selectorBasedListenersByCommandName[commandName]
|
||||
const selectorListener = new SelectorBasedListener(selector, commandName, listener)
|
||||
listenersForCommand.push(selectorListener)
|
||||
|
||||
this.commandRegistered(commandName)
|
||||
|
||||
return new Disposable(() => {
|
||||
listenersForCommand.splice(listenersForCommand.indexOf(selectorListener), 1)
|
||||
if (listenersForCommand.length === 0) {
|
||||
delete this.selectorBasedListenersByCommandName[commandName]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
addInlineListener (element, commandName, listener) {
|
||||
if (this.inlineListenersByCommandName[commandName] == null) {
|
||||
this.inlineListenersByCommandName[commandName] = new WeakMap()
|
||||
}
|
||||
|
||||
const listenersForCommand = this.inlineListenersByCommandName[commandName]
|
||||
let listenersForElement = listenersForCommand.get(element)
|
||||
if (!listenersForElement) {
|
||||
listenersForElement = []
|
||||
listenersForCommand.set(element, listenersForElement)
|
||||
}
|
||||
const inlineListener = new InlineListener(commandName, listener)
|
||||
listenersForElement.push(inlineListener)
|
||||
|
||||
this.commandRegistered(commandName)
|
||||
|
||||
return new Disposable(() => {
|
||||
listenersForElement.splice(listenersForElement.indexOf(inlineListener), 1)
|
||||
if (listenersForElement.length === 0) {
|
||||
listenersForCommand.delete(element)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Public: Find all registered commands matching a query.
|
||||
//
|
||||
// * `params` An {Object} containing one or more of the following keys:
|
||||
// * `target` A DOM node that is the hypothetical target of a given command.
|
||||
//
|
||||
// Returns an {Array} of `CommandDescriptor` {Object}s containing the following keys:
|
||||
// * `name` The name of the command. For example, `user:insert-date`.
|
||||
// * `displayName` The display name of the command. For example,
|
||||
// `User: Insert Date`.
|
||||
// Additional metadata may also be present in the returned descriptor:
|
||||
// * `description` a {String} describing the function of the command in more
|
||||
// detail than the title
|
||||
// * `tags` an {Array} of {String}s that describe keywords related to the
|
||||
// command
|
||||
// Any additional nonstandard metadata provided when the command was `add`ed
|
||||
// may also be present in the returned descriptor.
|
||||
findCommands ({ target }) {
|
||||
const commandNames = new Set()
|
||||
const commands = []
|
||||
let currentTarget = target
|
||||
while (true) {
|
||||
let listeners
|
||||
for (const name in this.inlineListenersByCommandName) {
|
||||
listeners = this.inlineListenersByCommandName[name]
|
||||
if (listeners.has(currentTarget) && !commandNames.has(name)) {
|
||||
commandNames.add(name)
|
||||
const targetListeners = listeners.get(currentTarget)
|
||||
commands.push(
|
||||
...targetListeners.map(listener => listener.descriptor)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (const commandName in this.selectorBasedListenersByCommandName) {
|
||||
listeners = this.selectorBasedListenersByCommandName[commandName]
|
||||
for (const listener of listeners) {
|
||||
if (listener.matchesTarget(currentTarget)) {
|
||||
if (!commandNames.has(commandName)) {
|
||||
commandNames.add(commandName)
|
||||
commands.push(listener.descriptor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTarget === window) {
|
||||
break
|
||||
}
|
||||
currentTarget = currentTarget.parentNode || window
|
||||
}
|
||||
|
||||
return commands
|
||||
}
|
||||
|
||||
// Public: Simulate the dispatch of a command on a DOM node.
|
||||
//
|
||||
// This can be useful for testing when you want to simulate the invocation of a
|
||||
// command on a detached DOM node. Otherwise, the DOM node in question needs to
|
||||
// be attached to the document so the event bubbles up to the root node to be
|
||||
// processed.
|
||||
//
|
||||
// * `target` The DOM node at which to start bubbling the command event.
|
||||
// * `commandName` {String} indicating the name of the command to dispatch.
|
||||
dispatch (target, commandName, detail) {
|
||||
const event = new CustomEvent(commandName, { bubbles: true, detail })
|
||||
Object.defineProperty(event, 'target', { value: target })
|
||||
return this.handleCommandEvent(event)
|
||||
}
|
||||
|
||||
// Public: Invoke the given callback before dispatching a command event.
|
||||
//
|
||||
// * `callback` {Function} to be called before dispatching each command
|
||||
// * `event` The Event that will be dispatched
|
||||
onWillDispatch (callback) {
|
||||
return this.emitter.on('will-dispatch', callback)
|
||||
}
|
||||
|
||||
// Public: Invoke the given callback after dispatching a command event.
|
||||
//
|
||||
// * `callback` {Function} to be called after dispatching each command
|
||||
// * `event` The Event that was dispatched
|
||||
onDidDispatch (callback) {
|
||||
return this.emitter.on('did-dispatch', callback)
|
||||
}
|
||||
|
||||
getSnapshot () {
|
||||
const snapshot = {}
|
||||
for (const commandName in this.selectorBasedListenersByCommandName) {
|
||||
const listeners = this.selectorBasedListenersByCommandName[commandName]
|
||||
snapshot[commandName] = listeners.slice()
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
restoreSnapshot (snapshot) {
|
||||
this.selectorBasedListenersByCommandName = {}
|
||||
for (const commandName in snapshot) {
|
||||
const listeners = snapshot[commandName]
|
||||
this.selectorBasedListenersByCommandName[commandName] = listeners.slice()
|
||||
}
|
||||
}
|
||||
|
||||
handleCommandEvent (event) {
|
||||
let propagationStopped = false
|
||||
let immediatePropagationStopped = false
|
||||
let matched = []
|
||||
let currentTarget = event.target
|
||||
|
||||
const dispatchedEvent = new CustomEvent(event.type, {
|
||||
bubbles: true,
|
||||
detail: event.detail
|
||||
})
|
||||
Object.defineProperty(dispatchedEvent, 'eventPhase', {
|
||||
value: Event.BUBBLING_PHASE
|
||||
})
|
||||
Object.defineProperty(dispatchedEvent, 'currentTarget', {
|
||||
get () {
|
||||
return currentTarget
|
||||
}
|
||||
})
|
||||
Object.defineProperty(dispatchedEvent, 'target', { value: currentTarget })
|
||||
Object.defineProperty(dispatchedEvent, 'preventDefault', {
|
||||
value () {
|
||||
return event.preventDefault()
|
||||
}
|
||||
})
|
||||
Object.defineProperty(dispatchedEvent, 'stopPropagation', {
|
||||
value () {
|
||||
event.stopPropagation()
|
||||
propagationStopped = true
|
||||
}
|
||||
})
|
||||
Object.defineProperty(dispatchedEvent, 'stopImmediatePropagation', {
|
||||
value () {
|
||||
event.stopImmediatePropagation()
|
||||
propagationStopped = true
|
||||
immediatePropagationStopped = true
|
||||
}
|
||||
})
|
||||
Object.defineProperty(dispatchedEvent, 'abortKeyBinding', {
|
||||
value () {
|
||||
if (typeof event.abortKeyBinding === 'function') {
|
||||
event.abortKeyBinding()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
for (const key of Object.keys(event)) {
|
||||
if (!(key in dispatchedEvent)) {
|
||||
dispatchedEvent[key] = event[key]
|
||||
}
|
||||
}
|
||||
|
||||
this.emitter.emit('will-dispatch', dispatchedEvent)
|
||||
|
||||
while (true) {
|
||||
const commandInlineListeners =
|
||||
this.inlineListenersByCommandName[event.type]
|
||||
? this.inlineListenersByCommandName[event.type].get(currentTarget)
|
||||
: null
|
||||
let listeners = commandInlineListeners || []
|
||||
if (currentTarget.webkitMatchesSelector != null) {
|
||||
const selectorBasedListeners =
|
||||
(this.selectorBasedListenersByCommandName[event.type] || [])
|
||||
.filter(listener => listener.matchesTarget(currentTarget))
|
||||
.sort((a, b) => a.compare(b))
|
||||
listeners = selectorBasedListeners.concat(listeners)
|
||||
}
|
||||
|
||||
// Call inline listeners first in reverse registration order,
|
||||
// and selector-based listeners by specificity and reverse
|
||||
// registration order.
|
||||
for (let i = listeners.length - 1; i >= 0; i--) {
|
||||
const listener = listeners[i]
|
||||
if (immediatePropagationStopped) {
|
||||
break
|
||||
}
|
||||
matched.push(listener.didDispatch.call(currentTarget, dispatchedEvent))
|
||||
}
|
||||
|
||||
if (currentTarget === window) {
|
||||
break
|
||||
}
|
||||
if (propagationStopped) {
|
||||
break
|
||||
}
|
||||
currentTarget = currentTarget.parentNode || window
|
||||
}
|
||||
|
||||
this.emitter.emit('did-dispatch', dispatchedEvent)
|
||||
|
||||
return (matched.length > 0 ? Promise.all(matched) : null)
|
||||
}
|
||||
|
||||
commandRegistered (commandName) {
|
||||
if (this.rootNode != null && !this.registeredCommands[commandName]) {
|
||||
this.rootNode.addEventListener(commandName, this.handleCommandEvent, true)
|
||||
return (this.registeredCommands[commandName] = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// type Listener = {
|
||||
// descriptor: CommandDescriptor,
|
||||
// extractDidDispatch: (e: CustomEvent) => void,
|
||||
// };
|
||||
class SelectorBasedListener {
|
||||
constructor (selector, commandName, listener) {
|
||||
this.selector = selector
|
||||
this.didDispatch = extractDidDispatch(listener)
|
||||
this.descriptor = extractDescriptor(commandName, listener)
|
||||
this.specificity = calculateSpecificity(this.selector)
|
||||
this.sequenceNumber = SequenceCount++
|
||||
}
|
||||
|
||||
compare (other) {
|
||||
return (
|
||||
this.specificity - other.specificity ||
|
||||
this.sequenceNumber - other.sequenceNumber
|
||||
)
|
||||
}
|
||||
|
||||
matchesTarget (target) {
|
||||
return target.webkitMatchesSelector && target.webkitMatchesSelector(this.selector)
|
||||
}
|
||||
}
|
||||
|
||||
class InlineListener {
|
||||
constructor (commandName, listener) {
|
||||
this.didDispatch = extractDidDispatch(listener)
|
||||
this.descriptor = extractDescriptor(commandName, listener)
|
||||
}
|
||||
}
|
||||
|
||||
// type CommandDescriptor = {
|
||||
// name: string,
|
||||
// displayName: string,
|
||||
// };
|
||||
function extractDescriptor (name, listener) {
|
||||
return Object.assign(
|
||||
_.omit(listener, 'didDispatch'),
|
||||
{
|
||||
name,
|
||||
displayName: listener.displayName ? listener.displayName : _.humanizeEventName(name)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function extractDidDispatch (listener) {
|
||||
return typeof listener === 'function' ? listener : listener.didDispatch
|
||||
}
|
||||
@@ -7,12 +7,28 @@
|
||||
|
||||
var path = require('path')
|
||||
var fs = require('fs-plus')
|
||||
var sourceMapSupport = require('@atom/source-map-support')
|
||||
|
||||
var PackageTranspilationRegistry = require('./package-transpilation-registry')
|
||||
var CSON = null
|
||||
|
||||
var packageTranspilationRegistry = new PackageTranspilationRegistry()
|
||||
|
||||
var COMPILERS = {
|
||||
'.js': require('./babel'),
|
||||
'.ts': require('./typescript'),
|
||||
'.coffee': require('./coffee-script')
|
||||
'.js': packageTranspilationRegistry.wrapTranspiler(require('./babel')),
|
||||
'.ts': packageTranspilationRegistry.wrapTranspiler(require('./typescript')),
|
||||
'.tsx': packageTranspilationRegistry.wrapTranspiler(require('./typescript')),
|
||||
'.coffee': packageTranspilationRegistry.wrapTranspiler(require('./coffee-script'))
|
||||
}
|
||||
|
||||
exports.addTranspilerConfigForPath = function (packagePath, packageName, packageMeta, config) {
|
||||
packagePath = fs.realpathSync(packagePath)
|
||||
packageTranspilationRegistry.addTranspilerConfigForPath(packagePath, packageName, packageMeta, config)
|
||||
}
|
||||
|
||||
exports.removeTranspilerConfigForPath = function (packagePath) {
|
||||
packagePath = fs.realpathSync(packagePath)
|
||||
packageTranspilationRegistry.removeTranspilerConfigForPath(packagePath)
|
||||
}
|
||||
|
||||
var cacheStats = {}
|
||||
@@ -43,11 +59,11 @@ exports.addPathToCache = function (filePath, atomHome) {
|
||||
CSON = require('season')
|
||||
CSON.setCacheDir(this.getCacheDirectory())
|
||||
}
|
||||
CSON.readFileSync(filePath)
|
||||
return CSON.readFileSync(filePath)
|
||||
} else {
|
||||
var compiler = COMPILERS[extension]
|
||||
if (compiler) {
|
||||
compileFileAtPath(compiler, filePath, extension)
|
||||
return compileFileAtPath(compiler, filePath, extension)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,20 +85,20 @@ function compileFileAtPath (compiler, filePath, extension) {
|
||||
var sourceCode = fs.readFileSync(filePath, 'utf8')
|
||||
if (compiler.shouldCompile(sourceCode, filePath)) {
|
||||
var cachePath = compiler.getCachePath(sourceCode, filePath)
|
||||
var compiledCode = readCachedJavascript(cachePath)
|
||||
var compiledCode = readCachedJavaScript(cachePath)
|
||||
if (compiledCode != null) {
|
||||
cacheStats[extension].hits++
|
||||
} else {
|
||||
cacheStats[extension].misses++
|
||||
compiledCode = addSourceURL(compiler.compile(sourceCode, filePath), filePath)
|
||||
writeCachedJavascript(cachePath, compiledCode)
|
||||
compiledCode = compiler.compile(sourceCode, filePath)
|
||||
writeCachedJavaScript(cachePath, compiledCode)
|
||||
}
|
||||
return compiledCode
|
||||
}
|
||||
return sourceCode
|
||||
}
|
||||
|
||||
function readCachedJavascript (relativeCachePath) {
|
||||
function readCachedJavaScript (relativeCachePath) {
|
||||
var cachePath = path.join(cacheDirectory, relativeCachePath)
|
||||
if (fs.isFileSync(cachePath)) {
|
||||
try {
|
||||
@@ -92,122 +108,135 @@ function readCachedJavascript (relativeCachePath) {
|
||||
return null
|
||||
}
|
||||
|
||||
function writeCachedJavascript (relativeCachePath, code) {
|
||||
function writeCachedJavaScript (relativeCachePath, code) {
|
||||
var cachePath = path.join(cacheDirectory, relativeCachePath)
|
||||
fs.writeFileSync(cachePath, code, 'utf8')
|
||||
}
|
||||
|
||||
function addSourceURL (jsCode, filePath) {
|
||||
if (process.platform === 'win32') {
|
||||
filePath = '/' + path.resolve(filePath).replace(/\\/g, '/')
|
||||
}
|
||||
return jsCode + '\n' + '//# sourceURL=' + encodeURI(filePath) + '\n'
|
||||
}
|
||||
|
||||
var INLINE_SOURCE_MAP_REGEXP = /\/\/[#@]\s*sourceMappingURL=([^'"\n]+)\s*$/mg
|
||||
|
||||
require('source-map-support').install({
|
||||
handleUncaughtExceptions: false,
|
||||
|
||||
// Most of this logic is the same as the default implementation in the
|
||||
// source-map-support module, but we've overridden it to read the javascript
|
||||
// code from our cache directory.
|
||||
retrieveSourceMap: function (filePath) {
|
||||
if (!cacheDirectory || !fs.isFileSync(filePath)) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
var sourceCode = fs.readFileSync(filePath, 'utf8')
|
||||
} catch (error) {
|
||||
console.warn('Error reading source file', error.stack)
|
||||
return null
|
||||
}
|
||||
|
||||
var compiler = COMPILERS[path.extname(filePath)]
|
||||
|
||||
try {
|
||||
var fileData = readCachedJavascript(compiler.getCachePath(sourceCode, filePath))
|
||||
} catch (error) {
|
||||
console.warn('Error reading compiled file', error.stack)
|
||||
return null
|
||||
}
|
||||
|
||||
if (fileData == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
var match, lastMatch
|
||||
INLINE_SOURCE_MAP_REGEXP.lastIndex = 0
|
||||
while ((match = INLINE_SOURCE_MAP_REGEXP.exec(fileData))) {
|
||||
lastMatch = match
|
||||
}
|
||||
if (lastMatch == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
var sourceMappingURL = lastMatch[1]
|
||||
var rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(',') + 1)
|
||||
|
||||
try {
|
||||
var sourceMap = JSON.parse(new Buffer(rawData, 'base64'))
|
||||
} catch (error) {
|
||||
console.warn('Error parsing source map', error.stack)
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
map: sourceMap,
|
||||
url: null
|
||||
exports.install = function (resourcesPath, nodeRequire) {
|
||||
const snapshotSourceMapConsumer = {
|
||||
originalPositionFor ({line, column}) {
|
||||
const {relativePath, row} = snapshotResult.translateSnapshotRow(line)
|
||||
return {
|
||||
column,
|
||||
line: row,
|
||||
source: path.join(resourcesPath, 'app', 'static', relativePath),
|
||||
name: null
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
var prepareStackTraceWithSourceMapping = Error.prepareStackTrace
|
||||
var prepareStackTrace = prepareStackTraceWithSourceMapping
|
||||
sourceMapSupport.install({
|
||||
handleUncaughtExceptions: false,
|
||||
|
||||
function prepareStackTraceWithRawStackAssignment (error, frames) {
|
||||
if (error.rawStack) { // avoid infinite recursion
|
||||
return prepareStackTraceWithSourceMapping(error, frames)
|
||||
} else {
|
||||
error.rawStack = frames
|
||||
return prepareStackTrace(error, frames)
|
||||
}
|
||||
}
|
||||
// Most of this logic is the same as the default implementation in the
|
||||
// source-map-support module, but we've overridden it to read the javascript
|
||||
// code from our cache directory.
|
||||
retrieveSourceMap: function (filePath) {
|
||||
if (filePath === '<embedded>') {
|
||||
return {map: snapshotSourceMapConsumer}
|
||||
}
|
||||
|
||||
Error.stackTraceLimit = 30
|
||||
if (!cacheDirectory || !fs.isFileSync(filePath)) {
|
||||
return null
|
||||
}
|
||||
|
||||
Object.defineProperty(Error, 'prepareStackTrace', {
|
||||
get: function () {
|
||||
return prepareStackTraceWithRawStackAssignment
|
||||
},
|
||||
try {
|
||||
var sourceCode = fs.readFileSync(filePath, 'utf8')
|
||||
} catch (error) {
|
||||
console.warn('Error reading source file', error.stack)
|
||||
return null
|
||||
}
|
||||
|
||||
set: function (newValue) {
|
||||
prepareStackTrace = newValue
|
||||
process.nextTick(function () {
|
||||
prepareStackTrace = prepareStackTraceWithSourceMapping
|
||||
})
|
||||
}
|
||||
})
|
||||
var compiler = COMPILERS[path.extname(filePath)]
|
||||
if (!compiler) compiler = COMPILERS['.js']
|
||||
|
||||
Error.prototype.getRawStack = function () { // eslint-disable-line no-extend-native
|
||||
// Access this.stack to ensure prepareStackTrace has been run on this error
|
||||
// because it assigns this.rawStack as a side-effect
|
||||
this.stack
|
||||
return this.rawStack
|
||||
}
|
||||
try {
|
||||
var fileData = readCachedJavaScript(compiler.getCachePath(sourceCode, filePath))
|
||||
} catch (error) {
|
||||
console.warn('Error reading compiled file', error.stack)
|
||||
return null
|
||||
}
|
||||
|
||||
Object.keys(COMPILERS).forEach(function (extension) {
|
||||
var compiler = COMPILERS[extension]
|
||||
if (fileData == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
Object.defineProperty(require.extensions, extension, {
|
||||
enumerable: true,
|
||||
writable: false,
|
||||
value: function (module, filePath) {
|
||||
var code = compileFileAtPath(compiler, filePath, extension)
|
||||
return module._compile(code, filePath)
|
||||
var match, lastMatch
|
||||
INLINE_SOURCE_MAP_REGEXP.lastIndex = 0
|
||||
while ((match = INLINE_SOURCE_MAP_REGEXP.exec(fileData))) {
|
||||
lastMatch = match
|
||||
}
|
||||
if (lastMatch == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
var sourceMappingURL = lastMatch[1]
|
||||
var rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(',') + 1)
|
||||
|
||||
try {
|
||||
var sourceMap = JSON.parse(new Buffer(rawData, 'base64'))
|
||||
} catch (error) {
|
||||
console.warn('Error parsing source map', error.stack)
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
map: sourceMap,
|
||||
url: null
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
var prepareStackTraceWithSourceMapping = Error.prepareStackTrace
|
||||
var prepareStackTrace = prepareStackTraceWithSourceMapping
|
||||
|
||||
function prepareStackTraceWithRawStackAssignment (error, frames) {
|
||||
if (error.rawStack) { // avoid infinite recursion
|
||||
return prepareStackTraceWithSourceMapping(error, frames)
|
||||
} else {
|
||||
error.rawStack = frames
|
||||
return prepareStackTrace(error, frames)
|
||||
}
|
||||
}
|
||||
|
||||
Error.stackTraceLimit = 30
|
||||
|
||||
Object.defineProperty(Error, 'prepareStackTrace', {
|
||||
get: function () {
|
||||
return prepareStackTraceWithRawStackAssignment
|
||||
},
|
||||
|
||||
set: function (newValue) {
|
||||
prepareStackTrace = newValue
|
||||
process.nextTick(function () {
|
||||
prepareStackTrace = prepareStackTraceWithSourceMapping
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
Error.prototype.getRawStack = function () { // eslint-disable-line no-extend-native
|
||||
// Access this.stack to ensure prepareStackTrace has been run on this error
|
||||
// because it assigns this.rawStack as a side-effect
|
||||
this.stack
|
||||
return this.rawStack
|
||||
}
|
||||
|
||||
Object.keys(COMPILERS).forEach(function (extension) {
|
||||
var compiler = COMPILERS[extension]
|
||||
|
||||
Object.defineProperty(nodeRequire.extensions, extension, {
|
||||
enumerable: true,
|
||||
writable: false,
|
||||
value: function (module, filePath) {
|
||||
var code = compileFileAtPath(compiler, filePath, extension)
|
||||
return module._compile(code, filePath)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
exports.supportedExtensions = Object.keys(COMPILERS)
|
||||
exports.resetCacheStats()
|
||||
|
||||
145
src/config-file.js
Normal file
145
src/config-file.js
Normal file
@@ -0,0 +1,145 @@
|
||||
const _ = require('underscore-plus')
|
||||
const fs = require('fs-plus')
|
||||
const dedent = require('dedent')
|
||||
const {Emitter} = require('event-kit')
|
||||
const {watchPath} = require('./path-watcher')
|
||||
const CSON = require('season')
|
||||
const Path = require('path')
|
||||
const async = require('async')
|
||||
const temp = require('temp')
|
||||
|
||||
const EVENT_TYPES = new Set([
|
||||
'created',
|
||||
'modified',
|
||||
'renamed'
|
||||
])
|
||||
|
||||
module.exports =
|
||||
class ConfigFile {
|
||||
static at (path) {
|
||||
if (!this._known) {
|
||||
this._known = new Map()
|
||||
}
|
||||
|
||||
const existing = this._known.get(path)
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
|
||||
const created = new ConfigFile(path)
|
||||
this._known.set(path, created)
|
||||
return created
|
||||
}
|
||||
|
||||
constructor (path) {
|
||||
this.path = path
|
||||
this.emitter = new Emitter()
|
||||
this.value = {}
|
||||
this.reloadCallbacks = []
|
||||
|
||||
// Use a queue to prevent multiple concurrent write to the same file.
|
||||
const writeQueue = async.queue((data, callback) => {
|
||||
(async () => {
|
||||
try {
|
||||
await writeCSONFileAtomically(this.path, data)
|
||||
} catch (error) {
|
||||
this.emitter.emit('did-error', dedent `
|
||||
Failed to write \`${Path.basename(this.path)}\`.
|
||||
|
||||
${error.message}
|
||||
`)
|
||||
}
|
||||
callback()
|
||||
})()
|
||||
})
|
||||
|
||||
this.requestLoad = _.debounce(() => this.reload(), 200)
|
||||
this.requestSave = _.debounce((data) => writeQueue.push(data), 200)
|
||||
}
|
||||
|
||||
get () {
|
||||
return this.value
|
||||
}
|
||||
|
||||
update (value) {
|
||||
return new Promise(resolve => {
|
||||
this.requestSave(value)
|
||||
this.reloadCallbacks.push(resolve)
|
||||
})
|
||||
}
|
||||
|
||||
async watch (callback) {
|
||||
if (!fs.existsSync(this.path)) {
|
||||
fs.makeTreeSync(Path.dirname(this.path))
|
||||
CSON.writeFileSync(this.path, {}, {flag: 'wx'})
|
||||
}
|
||||
|
||||
await this.reload()
|
||||
|
||||
try {
|
||||
const watcher = await watchPath(this.path, {}, events => {
|
||||
if (events.some(event => EVENT_TYPES.has(event.action))) this.requestLoad()
|
||||
})
|
||||
return watcher
|
||||
} catch (error) {
|
||||
this.emitter.emit('did-error', dedent `
|
||||
Unable to watch path: \`${Path.basename(this.path)}\`.
|
||||
|
||||
Make sure you have permissions to \`${this.path}\`.
|
||||
On linux there are currently problems with watch sizes.
|
||||
See [this document][watches] for more info.
|
||||
|
||||
[watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path\
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
onDidChange (callback) {
|
||||
return this.emitter.on('did-change', callback)
|
||||
}
|
||||
|
||||
onDidError (callback) {
|
||||
return this.emitter.on('did-error', callback)
|
||||
}
|
||||
|
||||
reload () {
|
||||
return new Promise(resolve => {
|
||||
CSON.readFile(this.path, (error, data) => {
|
||||
if (error) {
|
||||
this.emitter.emit('did-error', `Failed to load \`${Path.basename(this.path)}\` - ${error.message}`)
|
||||
} else {
|
||||
this.value = data || {}
|
||||
this.emitter.emit('did-change', this.value)
|
||||
|
||||
for (const callback of this.reloadCallbacks) callback()
|
||||
this.reloadCallbacks.length = 0
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function writeCSONFile (path, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
CSON.writeFile(path, data, error => {
|
||||
if (error) reject(error)
|
||||
else resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function writeCSONFileAtomically (path, data) {
|
||||
const tempPath = temp.path()
|
||||
await writeCSONFile(tempPath, data)
|
||||
await rename(tempPath, path)
|
||||
}
|
||||
|
||||
function rename (oldPath, newPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.rename(oldPath, newPath, error => {
|
||||
if (error) reject(error)
|
||||
else resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
path = require 'path'
|
||||
fs = require 'fs-plus'
|
||||
|
||||
# This is loaded by atom.coffee. See https://atom.io/docs/api/latest/Config for
|
||||
# more information about config schemas.
|
||||
module.exports =
|
||||
core:
|
||||
type: 'object'
|
||||
properties:
|
||||
ignoredNames:
|
||||
type: 'array'
|
||||
default: [".git", ".hg", ".svn", ".DS_Store", "._*", "Thumbs.db"]
|
||||
items:
|
||||
type: 'string'
|
||||
description: 'List of string glob patterns. Files and directories matching these patterns will be ignored by some packages, such as the fuzzy finder and tree view. Individual packages might have additional config settings for ignoring names.'
|
||||
excludeVcsIgnoredPaths:
|
||||
type: 'boolean'
|
||||
default: true
|
||||
title: 'Exclude VCS Ignored Paths'
|
||||
description: 'Files and directories ignored by the current project\'s VCS system will be ignored by some packages, such as the fuzzy finder and find and replace. For example, projects using Git have these paths defined in the .gitignore file. Individual packages might have additional config settings for ignoring VCS ignored files and folders.'
|
||||
followSymlinks:
|
||||
type: 'boolean'
|
||||
default: true
|
||||
description: 'Follow symbolic links when searching files and when opening files with the fuzzy finder.'
|
||||
disabledPackages:
|
||||
type: 'array'
|
||||
default: []
|
||||
items:
|
||||
type: 'string'
|
||||
description: 'List of names of installed packages which are not loaded at startup.'
|
||||
customFileTypes:
|
||||
type: 'object'
|
||||
default: {}
|
||||
description: 'Associates scope names (e.g. `"source.js"`) with arrays of file extensions and file names (e.g. `["Somefile", ".js2"]`)'
|
||||
additionalProperties:
|
||||
type: 'array'
|
||||
items:
|
||||
type: 'string'
|
||||
themes:
|
||||
type: 'array'
|
||||
default: ['one-dark-ui', 'one-dark-syntax']
|
||||
items:
|
||||
type: 'string'
|
||||
description: 'Names of UI and syntax themes which will be used when Atom starts.'
|
||||
projectHome:
|
||||
type: 'string'
|
||||
default: path.join(fs.getHomeDirectory(), 'github')
|
||||
description: 'The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default.'
|
||||
audioBeep:
|
||||
type: 'boolean'
|
||||
default: true
|
||||
description: 'Trigger the system\'s beep sound when certain actions cannot be executed or there are no results.'
|
||||
destroyEmptyPanes:
|
||||
type: 'boolean'
|
||||
default: true
|
||||
title: 'Remove Empty Panes'
|
||||
description: 'When the last tab of a pane is closed, remove that pane as well.'
|
||||
closeEmptyWindows:
|
||||
type: 'boolean'
|
||||
default: true
|
||||
description: 'When a window with no open tabs or panes is given the \'Close Tab\' command, close that window.'
|
||||
fileEncoding:
|
||||
description: 'Default character set encoding to use when reading and writing files.'
|
||||
type: 'string'
|
||||
default: 'utf8'
|
||||
enum: [
|
||||
'cp437',
|
||||
'eucjp',
|
||||
'euckr',
|
||||
'gbk',
|
||||
'iso88591',
|
||||
'iso885910',
|
||||
'iso885913',
|
||||
'iso885914',
|
||||
'iso885915',
|
||||
'iso885916',
|
||||
'iso88592',
|
||||
'iso88593',
|
||||
'iso88594',
|
||||
'iso88595',
|
||||
'iso88596',
|
||||
'iso88597',
|
||||
'iso88597',
|
||||
'iso88598',
|
||||
'koi8r',
|
||||
'koi8u',
|
||||
'macroman',
|
||||
'shiftjis',
|
||||
'utf16be',
|
||||
'utf16le',
|
||||
'utf8',
|
||||
'windows1250',
|
||||
'windows1251',
|
||||
'windows1252',
|
||||
'windows1253',
|
||||
'windows1254',
|
||||
'windows1255',
|
||||
'windows1256',
|
||||
'windows1257',
|
||||
'windows1258',
|
||||
'windows866'
|
||||
]
|
||||
openEmptyEditorOnStart:
|
||||
description: 'Automatically open an empty editor on startup.'
|
||||
type: 'boolean'
|
||||
default: true
|
||||
automaticallyUpdate:
|
||||
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'
|
||||
properties:
|
||||
# These settings are used in scoped fashion only. No defaults.
|
||||
commentStart:
|
||||
type: ['string', 'null']
|
||||
commentEnd:
|
||||
type: ['string', 'null']
|
||||
increaseIndentPattern:
|
||||
type: ['string', 'null']
|
||||
decreaseIndentPattern:
|
||||
type: ['string', 'null']
|
||||
foldEndPattern:
|
||||
type: ['string', 'null']
|
||||
|
||||
# These can be used as globals or scoped, thus defaults.
|
||||
fontFamily:
|
||||
type: 'string'
|
||||
default: ''
|
||||
description: 'The name of the font family used for editor text.'
|
||||
fontSize:
|
||||
type: 'integer'
|
||||
default: 14
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
description: 'Height in pixels of editor text.'
|
||||
lineHeight:
|
||||
type: ['string', 'number']
|
||||
default: 1.5
|
||||
description: 'Height of editor lines, as a multiplier of font size.'
|
||||
showInvisibles:
|
||||
type: 'boolean'
|
||||
default: false
|
||||
description: 'Render placeholders for invisible characters, such as tabs, spaces and newlines.'
|
||||
showIndentGuide:
|
||||
type: 'boolean'
|
||||
default: false
|
||||
description: 'Show indentation indicators in the editor.'
|
||||
showLineNumbers:
|
||||
type: 'boolean'
|
||||
default: true
|
||||
description: 'Show line numbers in the editor\'s gutter.'
|
||||
autoIndent:
|
||||
type: 'boolean'
|
||||
default: true
|
||||
description: 'Automatically indent the cursor when inserting a newline.'
|
||||
autoIndentOnPaste:
|
||||
type: 'boolean'
|
||||
default: true
|
||||
description: 'Automatically indent pasted text based on the indentation of the previous line.'
|
||||
nonWordCharacters:
|
||||
type: 'string'
|
||||
default: "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-…"
|
||||
description: 'A string of non-word characters to define word boundaries.'
|
||||
preferredLineLength:
|
||||
type: 'integer'
|
||||
default: 80
|
||||
minimum: 1
|
||||
description: 'Identifies the length of a line which is used when wrapping text with the `Soft Wrap At Preferred Line Length` setting enabled, in number of characters.'
|
||||
tabLength:
|
||||
type: 'integer'
|
||||
default: 2
|
||||
minimum: 1
|
||||
description: 'Number of spaces used to represent a tab.'
|
||||
softWrap:
|
||||
type: 'boolean'
|
||||
default: false
|
||||
description: 'Wraps lines that exceed the width of the window. When `Soft Wrap At Preferred Line Length` is set, it will wrap to the number of characters defined by the `Preferred Line Length` setting.'
|
||||
softTabs:
|
||||
type: 'boolean'
|
||||
default: true
|
||||
description: 'If the `Tab Type` config setting is set to "auto" and autodetection of tab type from buffer content fails, then this config setting determines whether a soft tab or a hard tab will be inserted when the Tab key is pressed.'
|
||||
tabType:
|
||||
type: 'string'
|
||||
default: 'auto'
|
||||
enum: ['auto', 'soft', 'hard']
|
||||
description: 'Determine character inserted when Tab key is pressed. Possible values: "auto", "soft" and "hard". When set to "soft" or "hard", soft tabs (spaces) or hard tabs (tab characters) are used. When set to "auto", the editor auto-detects the tab type based on the contents of the buffer (it uses the first leading whitespace on a non-comment line), or uses the value of the Soft Tabs config setting if auto-detection fails.'
|
||||
softWrapAtPreferredLineLength:
|
||||
type: 'boolean'
|
||||
default: false
|
||||
description: 'Instead of wrapping lines to the window\'s width, wrap lines to the number of characters defined by the `Preferred Line Length` setting. This will only take effect when the soft wrap config setting is enabled globally or for the current language.'
|
||||
softWrapHangingIndent:
|
||||
type: 'integer'
|
||||
default: 0
|
||||
minimum: 0
|
||||
description: 'When soft wrap is enabled, defines length of additional indentation applied to wrapped lines, in number of characters.'
|
||||
scrollSensitivity:
|
||||
type: 'integer'
|
||||
default: 40
|
||||
minimum: 10
|
||||
maximum: 200
|
||||
description: 'Determines how fast the editor scrolls when using a mouse or trackpad.'
|
||||
scrollPastEnd:
|
||||
type: 'boolean'
|
||||
default: false
|
||||
description: 'Allow the editor to be scrolled past the end of the last line.'
|
||||
undoGroupingInterval:
|
||||
type: 'integer'
|
||||
default: 300
|
||||
minimum: 0
|
||||
description: 'Time interval in milliseconds within which text editing operations will be grouped together in the undo history.'
|
||||
useShadowDOM:
|
||||
type: 'boolean'
|
||||
default: true
|
||||
title: 'Use Shadow DOM'
|
||||
description: 'Disable if you experience styling issues with packages or themes. Be sure to open an issue on the relevant package or theme, because this option is going away eventually.'
|
||||
confirmCheckoutHeadRevision:
|
||||
type: 'boolean'
|
||||
default: true
|
||||
title: 'Confirm Checkout HEAD Revision'
|
||||
description: 'Show confirmation dialog when checking out the HEAD revision and discarding changes to current file since last commit.'
|
||||
backUpBeforeSaving:
|
||||
type: 'boolean'
|
||||
default: false
|
||||
description: 'Ensure file contents aren\'t lost if there is an I/O error during save by making a temporary backup copy.'
|
||||
invisibles:
|
||||
type: 'object'
|
||||
description: 'A hash of characters Atom will use to render whitespace characters. Keys are whitespace character types, values are rendered characters (use value false to turn off individual whitespace character types).'
|
||||
properties:
|
||||
eol:
|
||||
type: ['boolean', 'string']
|
||||
default: '\u00ac'
|
||||
maximumLength: 1
|
||||
description: 'Character used to render newline characters (\\n) when the `Show Invisibles` setting is enabled. '
|
||||
space:
|
||||
type: ['boolean', 'string']
|
||||
default: '\u00b7'
|
||||
maximumLength: 1
|
||||
description: 'Character used to render leading and trailing space characters when the `Show Invisibles` setting is enabled.'
|
||||
tab:
|
||||
type: ['boolean', 'string']
|
||||
default: '\u00bb'
|
||||
maximumLength: 1
|
||||
description: 'Character used to render hard tab characters (\\t) when the `Show Invisibles` setting is enabled.'
|
||||
cr:
|
||||
type: ['boolean', 'string']
|
||||
default: '\u00a4'
|
||||
maximumLength: 1
|
||||
description: 'Character used to render carriage return characters (for Microsoft-style line endings) when the `Show Invisibles` setting is enabled.'
|
||||
zoomFontWhenCtrlScrolling:
|
||||
type: 'boolean'
|
||||
default: process.platform isnt 'darwin'
|
||||
description: 'Change the editor font size when pressing the Ctrl key and scrolling the mouse up/down.'
|
||||
|
||||
if process.platform in ['win32', 'linux']
|
||||
module.exports.core.properties.autoHideMenuBar =
|
||||
type: 'boolean'
|
||||
default: false
|
||||
description: 'Automatically hide the menu bar and toggle it by pressing Alt. This is only supported on Windows & Linux.'
|
||||
578
src/config-schema.js
Normal file
578
src/config-schema.js
Normal file
@@ -0,0 +1,578 @@
|
||||
// This is loaded by atom-environment.coffee. See
|
||||
// https://atom.io/docs/api/latest/Config for more information about config
|
||||
// schemas.
|
||||
const configSchema = {
|
||||
core: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
ignoredNames: {
|
||||
type: 'array',
|
||||
default: ['.git', '.hg', '.svn', '.DS_Store', '._*', 'Thumbs.db', 'desktop.ini'],
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
description: 'List of [glob patterns](https://en.wikipedia.org/wiki/Glob_%28programming%29). Files and directories matching these patterns will be ignored by some packages, such as the fuzzy finder and tree view. Individual packages might have additional config settings for ignoring names.'
|
||||
},
|
||||
excludeVcsIgnoredPaths: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
title: 'Exclude VCS Ignored Paths',
|
||||
description: 'Files and directories ignored by the current project\'s VCS will be ignored by some packages, such as the fuzzy finder and find and replace. For example, projects using Git have these paths defined in the .gitignore file. Individual packages might have additional config settings for ignoring VCS ignored files and folders.'
|
||||
},
|
||||
followSymlinks: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Follow symbolic links when searching files and when opening files with the fuzzy finder.'
|
||||
},
|
||||
disabledPackages: {
|
||||
type: 'array',
|
||||
default: [],
|
||||
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
|
||||
description: 'List of names of installed packages which are not loaded at startup.'
|
||||
},
|
||||
versionPinnedPackages: {
|
||||
type: 'array',
|
||||
default: [],
|
||||
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
|
||||
description: 'List of names of installed packages which are not automatically updated.'
|
||||
},
|
||||
customFileTypes: {
|
||||
type: 'object',
|
||||
default: {},
|
||||
description: 'Associates scope names (e.g. `"source.js"`) with arrays of file extensions and file names (e.g. `["Somefile", ".js2"]`)',
|
||||
additionalProperties: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
uriHandlerRegistration: {
|
||||
type: 'string',
|
||||
default: 'prompt',
|
||||
description: 'When should Atom register itself as the default handler for atom:// URIs',
|
||||
enum: [
|
||||
{
|
||||
value: 'prompt',
|
||||
description: 'Prompt to register Atom as the default atom:// URI handler'
|
||||
},
|
||||
{
|
||||
value: 'always',
|
||||
description: 'Always become the default atom:// URI handler automatically'
|
||||
},
|
||||
{
|
||||
value: 'never',
|
||||
description: 'Never become the default atom:// URI handler'
|
||||
}
|
||||
]
|
||||
},
|
||||
themes: {
|
||||
type: 'array',
|
||||
default: ['one-dark-ui', 'one-dark-syntax'],
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
description: 'Names of UI and syntax themes which will be used when Atom starts.'
|
||||
},
|
||||
audioBeep: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Trigger the system\'s beep sound when certain actions cannot be executed or there are no results.'
|
||||
},
|
||||
closeDeletedFileTabs: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
title: 'Close Deleted File Tabs',
|
||||
description: 'Close corresponding editors when a file is deleted outside Atom.'
|
||||
},
|
||||
destroyEmptyPanes: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
title: 'Remove Empty Panes',
|
||||
description: 'When the last tab of a pane is closed, remove that pane as well.'
|
||||
},
|
||||
closeEmptyWindows: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'When a window with no open tabs or panes is given the \'Close Tab\' command, close that window.'
|
||||
},
|
||||
fileEncoding: {
|
||||
description: 'Default character set encoding to use when reading and writing files.',
|
||||
type: 'string',
|
||||
default: 'utf8',
|
||||
enum: [
|
||||
{
|
||||
value: 'iso88596',
|
||||
description: 'Arabic (ISO 8859-6)'
|
||||
},
|
||||
{
|
||||
value: 'windows1256',
|
||||
description: 'Arabic (Windows 1256)'
|
||||
},
|
||||
{
|
||||
value: 'iso88594',
|
||||
description: 'Baltic (ISO 8859-4)'
|
||||
},
|
||||
{
|
||||
value: 'windows1257',
|
||||
description: 'Baltic (Windows 1257)'
|
||||
},
|
||||
{
|
||||
value: 'iso885914',
|
||||
description: 'Celtic (ISO 8859-14)'
|
||||
},
|
||||
{
|
||||
value: 'iso88592',
|
||||
description: 'Central European (ISO 8859-2)'
|
||||
},
|
||||
{
|
||||
value: 'windows1250',
|
||||
description: 'Central European (Windows 1250)'
|
||||
},
|
||||
{
|
||||
value: 'gb18030',
|
||||
description: 'Chinese (GB18030)'
|
||||
},
|
||||
{
|
||||
value: 'gbk',
|
||||
description: 'Chinese (GBK)'
|
||||
},
|
||||
{
|
||||
value: 'cp950',
|
||||
description: 'Traditional Chinese (Big5)'
|
||||
},
|
||||
{
|
||||
value: 'big5hkscs',
|
||||
description: 'Traditional Chinese (Big5-HKSCS)'
|
||||
},
|
||||
{
|
||||
value: 'cp866',
|
||||
description: 'Cyrillic (CP 866)'
|
||||
},
|
||||
{
|
||||
value: 'iso88595',
|
||||
description: 'Cyrillic (ISO 8859-5)'
|
||||
},
|
||||
{
|
||||
value: 'koi8r',
|
||||
description: 'Cyrillic (KOI8-R)'
|
||||
},
|
||||
{
|
||||
value: 'koi8u',
|
||||
description: 'Cyrillic (KOI8-U)'
|
||||
},
|
||||
{
|
||||
value: 'windows1251',
|
||||
description: 'Cyrillic (Windows 1251)'
|
||||
},
|
||||
{
|
||||
value: 'cp437',
|
||||
description: 'DOS (CP 437)'
|
||||
},
|
||||
{
|
||||
value: 'cp850',
|
||||
description: 'DOS (CP 850)'
|
||||
},
|
||||
{
|
||||
value: 'iso885913',
|
||||
description: 'Estonian (ISO 8859-13)'
|
||||
},
|
||||
{
|
||||
value: 'iso88597',
|
||||
description: 'Greek (ISO 8859-7)'
|
||||
},
|
||||
{
|
||||
value: 'windows1253',
|
||||
description: 'Greek (Windows 1253)'
|
||||
},
|
||||
{
|
||||
value: 'iso88598',
|
||||
description: 'Hebrew (ISO 8859-8)'
|
||||
},
|
||||
{
|
||||
value: 'windows1255',
|
||||
description: 'Hebrew (Windows 1255)'
|
||||
},
|
||||
{
|
||||
value: 'cp932',
|
||||
description: 'Japanese (CP 932)'
|
||||
},
|
||||
{
|
||||
value: 'eucjp',
|
||||
description: 'Japanese (EUC-JP)'
|
||||
},
|
||||
{
|
||||
value: 'shiftjis',
|
||||
description: 'Japanese (Shift JIS)'
|
||||
},
|
||||
{
|
||||
value: 'euckr',
|
||||
description: 'Korean (EUC-KR)'
|
||||
},
|
||||
{
|
||||
value: 'iso885910',
|
||||
description: 'Nordic (ISO 8859-10)'
|
||||
},
|
||||
{
|
||||
value: 'iso885916',
|
||||
description: 'Romanian (ISO 8859-16)'
|
||||
},
|
||||
{
|
||||
value: 'iso88599',
|
||||
description: 'Turkish (ISO 8859-9)'
|
||||
},
|
||||
{
|
||||
value: 'windows1254',
|
||||
description: 'Turkish (Windows 1254)'
|
||||
},
|
||||
{
|
||||
value: 'utf8',
|
||||
description: 'Unicode (UTF-8)'
|
||||
},
|
||||
{
|
||||
value: 'utf16le',
|
||||
description: 'Unicode (UTF-16 LE)'
|
||||
},
|
||||
{
|
||||
value: 'utf16be',
|
||||
description: 'Unicode (UTF-16 BE)'
|
||||
},
|
||||
{
|
||||
value: 'windows1258',
|
||||
description: 'Vietnamese (Windows 1258)'
|
||||
},
|
||||
{
|
||||
value: 'iso88591',
|
||||
description: 'Western (ISO 8859-1)'
|
||||
},
|
||||
{
|
||||
value: 'iso88593',
|
||||
description: 'Western (ISO 8859-3)'
|
||||
},
|
||||
{
|
||||
value: 'iso885915',
|
||||
description: 'Western (ISO 8859-15)'
|
||||
},
|
||||
{
|
||||
value: 'macroman',
|
||||
description: 'Western (Mac Roman)'
|
||||
},
|
||||
{
|
||||
value: 'windows1252',
|
||||
description: 'Western (Windows 1252)'
|
||||
}
|
||||
]
|
||||
},
|
||||
openEmptyEditorOnStart: {
|
||||
description: 'When checked opens an untitled editor when loading a blank environment (such as with _File > New Window_ or when "Restore Previous Windows On Start" is unchecked); otherwise no editor is opened when loading a blank environment. This setting has no effect when restoring a previous state.',
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
restorePreviousWindowsOnStart: {
|
||||
type: 'string',
|
||||
enum: ['no', 'yes', 'always'],
|
||||
default: 'yes',
|
||||
description: "When selected 'no', a blank environment is loaded. When selected 'yes' and Atom is started from the icon or `atom` by itself from the command line, restores the last state of all Atom windows; otherwise a blank environment is loaded. When selected 'always', restores the last state of all Atom windows always, no matter how Atom is started."
|
||||
},
|
||||
reopenProjectMenuCount: {
|
||||
description: 'How many recent projects to show in the Reopen Project menu.',
|
||||
type: 'integer',
|
||||
default: 15
|
||||
},
|
||||
automaticallyUpdate: {
|
||||
description: 'Automatically update Atom when a new release is available.',
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
useProxySettingsWhenCallingApm: {
|
||||
title: 'Use Proxy Settings When Calling APM',
|
||||
description: 'Use detected proxy settings when calling the `apm` command-line tool.',
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
allowPendingPaneItems: {
|
||||
description: 'Allow items to be previewed without adding them to a pane permanently, such as when single clicking files in the tree view.',
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
telemetryConsent: {
|
||||
description: 'Allow usage statistics and exception reports to be sent to the Atom team to help improve the product.',
|
||||
title: 'Send Telemetry to the Atom Team',
|
||||
type: 'string',
|
||||
default: 'undecided',
|
||||
enum: [
|
||||
{
|
||||
value: 'limited',
|
||||
description: 'Allow limited anonymous usage stats, exception and crash reporting'
|
||||
},
|
||||
{
|
||||
value: 'no',
|
||||
description: 'Do not send any telemetry data'
|
||||
},
|
||||
{
|
||||
value: 'undecided',
|
||||
description: 'Undecided (Atom will ask again next time it is launched)'
|
||||
}
|
||||
]
|
||||
},
|
||||
warnOnLargeFileLimit: {
|
||||
description: 'Warn before opening files larger than this number of megabytes.',
|
||||
type: 'number',
|
||||
default: 40
|
||||
},
|
||||
fileSystemWatcher: {
|
||||
description: 'Choose the underlying implementation used to watch for filesystem changes. Emulating changes will miss any events caused by applications other than Atom, but may help prevent crashes or freezes.',
|
||||
type: 'string',
|
||||
default: 'native',
|
||||
enum: [
|
||||
{
|
||||
value: 'native',
|
||||
description: 'Native operating system APIs'
|
||||
},
|
||||
{
|
||||
value: 'experimental',
|
||||
description: 'Experimental filesystem watching library'
|
||||
},
|
||||
{
|
||||
value: 'poll',
|
||||
description: 'Polling'
|
||||
},
|
||||
{
|
||||
value: 'atom',
|
||||
description: 'Emulated with Atom events'
|
||||
}
|
||||
]
|
||||
},
|
||||
useTreeSitterParsers: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Experimental: Use the new Tree-sitter parsing system for supported languages.'
|
||||
},
|
||||
colorProfile: {
|
||||
description: "Specify whether Atom should use the operating system's color profile (recommended) or an alternative color profile.<br>Changing this setting will require a relaunch of Atom to take effect.",
|
||||
type: 'string',
|
||||
default: 'default',
|
||||
enum: [
|
||||
{
|
||||
value: 'default',
|
||||
description: 'Use color profile configured in the operating system'
|
||||
},
|
||||
{
|
||||
value: 'srgb',
|
||||
description: 'Use sRGB color profile'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
editor: {
|
||||
type: 'object',
|
||||
// These settings are used in scoped fashion only. No defaults.
|
||||
properties: {
|
||||
commentStart: {
|
||||
type: ['string', 'null']
|
||||
},
|
||||
commentEnd: {
|
||||
type: ['string', 'null']
|
||||
},
|
||||
increaseIndentPattern: {
|
||||
type: ['string', 'null']
|
||||
},
|
||||
decreaseIndentPattern: {
|
||||
type: ['string', 'null']
|
||||
},
|
||||
foldEndPattern: {
|
||||
type: ['string', 'null']
|
||||
},
|
||||
// These can be used as globals or scoped, thus defaults.
|
||||
fontFamily: {
|
||||
type: 'string',
|
||||
default: 'Menlo, Consolas, DejaVu Sans Mono, monospace',
|
||||
description: 'The name of the font family used for editor text.'
|
||||
},
|
||||
fontSize: {
|
||||
type: 'integer',
|
||||
default: 14,
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
description: 'Height in pixels of editor text.'
|
||||
},
|
||||
lineHeight: {
|
||||
type: ['string', 'number'],
|
||||
default: 1.5,
|
||||
description: 'Height of editor lines, as a multiplier of font size.'
|
||||
},
|
||||
showCursorOnSelection: {
|
||||
type: 'boolean',
|
||||
'default': true,
|
||||
description: 'Show cursor while there is a selection.'
|
||||
},
|
||||
showInvisibles: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Render placeholders for invisible characters, such as tabs, spaces and newlines.'
|
||||
},
|
||||
showIndentGuide: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Show indentation indicators in the editor.'
|
||||
},
|
||||
showLineNumbers: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Show line numbers in the editor\'s gutter.'
|
||||
},
|
||||
atomicSoftTabs: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Skip over tab-length runs of leading whitespace when moving the cursor.'
|
||||
},
|
||||
autoIndent: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Automatically indent the cursor when inserting a newline.'
|
||||
},
|
||||
autoIndentOnPaste: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Automatically indent pasted text based on the indentation of the previous line.'
|
||||
},
|
||||
nonWordCharacters: {
|
||||
type: 'string',
|
||||
default: "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-…",
|
||||
description: 'A string of non-word characters to define word boundaries.'
|
||||
},
|
||||
preferredLineLength: {
|
||||
type: 'integer',
|
||||
default: 80,
|
||||
minimum: 1,
|
||||
description: 'Identifies the length of a line which is used when wrapping text with the `Soft Wrap At Preferred Line Length` setting enabled, in number of characters.'
|
||||
},
|
||||
maxScreenLineLength: {
|
||||
type: 'integer',
|
||||
default: 500,
|
||||
minimum: 500,
|
||||
description: 'Defines the maximum width of the editor window before soft wrapping is enforced, in number of characters.'
|
||||
},
|
||||
tabLength: {
|
||||
type: 'integer',
|
||||
default: 2,
|
||||
minimum: 1,
|
||||
description: 'Number of spaces used to represent a tab.'
|
||||
},
|
||||
softWrap: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Wraps lines that exceed the width of the window. When `Soft Wrap At Preferred Line Length` is set, it will wrap to the number of characters defined by the `Preferred Line Length` setting.'
|
||||
},
|
||||
softTabs: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'If the `Tab Type` config setting is set to "auto" and autodetection of tab type from buffer content fails, then this config setting determines whether a soft tab or a hard tab will be inserted when the Tab key is pressed.'
|
||||
},
|
||||
tabType: {
|
||||
type: 'string',
|
||||
default: 'auto',
|
||||
enum: ['auto', 'soft', 'hard'],
|
||||
description: 'Determine character inserted when Tab key is pressed. Possible values: "auto", "soft" and "hard". When set to "soft" or "hard", soft tabs (spaces) or hard tabs (tab characters) are used. When set to "auto", the editor auto-detects the tab type based on the contents of the buffer (it uses the first leading whitespace on a non-comment line), or uses the value of the Soft Tabs config setting if auto-detection fails.'
|
||||
},
|
||||
softWrapAtPreferredLineLength: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Instead of wrapping lines to the window\'s width, wrap lines to the number of characters defined by the `Preferred Line Length` setting. This will only take effect when the soft wrap config setting is enabled globally or for the current language. **Note:** If you want to hide the wrap guide (the vertical line) you can disable the `wrap-guide` package.'
|
||||
},
|
||||
softWrapHangingIndent: {
|
||||
type: 'integer',
|
||||
default: 0,
|
||||
minimum: 0,
|
||||
description: 'When soft wrap is enabled, defines length of additional indentation applied to wrapped lines, in number of characters.'
|
||||
},
|
||||
scrollSensitivity: {
|
||||
type: 'integer',
|
||||
default: 40,
|
||||
minimum: 10,
|
||||
maximum: 200,
|
||||
description: 'Determines how fast the editor scrolls when using a mouse or trackpad.'
|
||||
},
|
||||
scrollPastEnd: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Allow the editor to be scrolled past the end of the last line.'
|
||||
},
|
||||
undoGroupingInterval: {
|
||||
type: 'integer',
|
||||
default: 300,
|
||||
minimum: 0,
|
||||
description: 'Time interval in milliseconds within which text editing operations will be grouped together in the undo history.'
|
||||
},
|
||||
confirmCheckoutHeadRevision: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
title: 'Confirm Checkout HEAD Revision',
|
||||
description: 'Show confirmation dialog when checking out the HEAD revision and discarding changes to current file since last commit.'
|
||||
},
|
||||
invisibles: {
|
||||
type: 'object',
|
||||
description: 'A hash of characters Atom will use to render whitespace characters. Keys are whitespace character types, values are rendered characters (use value false to turn off individual whitespace character types).',
|
||||
properties: {
|
||||
eol: {
|
||||
type: ['boolean', 'string'],
|
||||
default: '¬',
|
||||
maximumLength: 1,
|
||||
description: 'Character used to render newline characters (\\n) when the `Show Invisibles` setting is enabled. '
|
||||
},
|
||||
space: {
|
||||
type: ['boolean', 'string'],
|
||||
default: '·',
|
||||
maximumLength: 1,
|
||||
description: 'Character used to render leading and trailing space characters when the `Show Invisibles` setting is enabled.'
|
||||
},
|
||||
tab: {
|
||||
type: ['boolean', 'string'],
|
||||
default: '»',
|
||||
maximumLength: 1,
|
||||
description: 'Character used to render hard tab characters (\\t) when the `Show Invisibles` setting is enabled.'
|
||||
},
|
||||
cr: {
|
||||
type: ['boolean', 'string'],
|
||||
default: '¤',
|
||||
maximumLength: 1,
|
||||
description: 'Character used to render carriage return characters (for Microsoft-style line endings) when the `Show Invisibles` setting is enabled.'
|
||||
}
|
||||
}
|
||||
},
|
||||
zoomFontWhenCtrlScrolling: {
|
||||
type: 'boolean',
|
||||
default: process.platform !== 'darwin',
|
||||
description: 'Change the editor font size when pressing the Ctrl key and scrolling the mouse up/down.'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (['win32', 'linux'].includes(process.platform)) {
|
||||
configSchema.core.properties.autoHideMenuBar = {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Automatically hide the menu bar and toggle it by pressing Alt. This is only supported on Windows & Linux.'
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
configSchema.core.properties.titleBar = {
|
||||
type: 'string',
|
||||
default: 'native',
|
||||
enum: ['native', 'custom', 'custom-inset', 'hidden'],
|
||||
description: 'Experimental: A `custom` title bar adapts to theme colors. Choosing `custom-inset` adds a bit more padding. The title bar can also be completely `hidden`.<br>Note: Switching to a custom or hidden title bar will compromise some functionality.<br>This setting will require a relaunch of Atom to take effect.'
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = configSchema
|
||||
1260
src/config.coffee
1260
src/config.coffee
File diff suppressed because it is too large
Load Diff
1484
src/config.js
Normal file
1484
src/config.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
_ = require 'underscore-plus'
|
||||
path = require 'path'
|
||||
CSON = require 'season'
|
||||
fs = require 'fs-plus'
|
||||
@@ -6,6 +5,7 @@ fs = require 'fs-plus'
|
||||
{Disposable} = require 'event-kit'
|
||||
{remote} = require 'electron'
|
||||
MenuHelpers = require './menu-helpers'
|
||||
{sortMenuItems} = require './menu-sort-helpers'
|
||||
|
||||
platformContextMenu = require('../package.json')?._atomMenu?['context-menu']
|
||||
|
||||
@@ -41,15 +41,17 @@ platformContextMenu = require('../package.json')?._atomMenu?['context-menu']
|
||||
# {::add} for more information.
|
||||
module.exports =
|
||||
class ContextMenuManager
|
||||
constructor: ({@resourcePath, @devMode, @keymapManager}) ->
|
||||
constructor: ({@keymapManager}) ->
|
||||
@definitions = {'.overlayer': []} # TODO: Remove once color picker package stops touching private data
|
||||
@clear()
|
||||
|
||||
@keymapManager.onDidLoadBundledKeymaps => @loadPlatformItems()
|
||||
|
||||
initialize: ({@resourcePath, @devMode}) ->
|
||||
|
||||
loadPlatformItems: ->
|
||||
if platformContextMenu?
|
||||
@add(platformContextMenu)
|
||||
@add(platformContextMenu, @devMode ? false)
|
||||
else
|
||||
menusDirPath = path.join(@resourcePath, 'menus')
|
||||
platformMenuPath = fs.resolve(menusDirPath, process.platform, ['cson', 'json'])
|
||||
@@ -108,11 +110,11 @@ class ContextMenuManager
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to remove the
|
||||
# added menu items.
|
||||
add: (itemsBySelector) ->
|
||||
add: (itemsBySelector, throwOnInvalidSelector = true) ->
|
||||
addedItemSets = []
|
||||
|
||||
for selector, items of itemsBySelector
|
||||
validateSelector(selector)
|
||||
validateSelector(selector) if throwOnInvalidSelector
|
||||
itemSet = new ContextMenuItemSet(selector, items)
|
||||
addedItemSets.push(itemSet)
|
||||
@itemSets.push(itemSet)
|
||||
@@ -145,7 +147,41 @@ class ContextMenuManager
|
||||
|
||||
currentTarget = currentTarget.parentElement
|
||||
|
||||
template
|
||||
@pruneRedundantSeparators(template)
|
||||
@addAccelerators(template)
|
||||
|
||||
return @sortTemplate(template)
|
||||
|
||||
# Adds an `accelerator` property to items that have key bindings. Electron
|
||||
# uses this property to surface the relevant keymaps in the context menu.
|
||||
addAccelerators: (template) ->
|
||||
for id, item of template
|
||||
if item.command
|
||||
keymaps = @keymapManager.findKeyBindings({command: item.command, target: document.activeElement})
|
||||
accelerator = MenuHelpers.acceleratorForKeystroke(keymaps?[0]?.keystrokes)
|
||||
item.accelerator = accelerator if accelerator
|
||||
if Array.isArray(item.submenu)
|
||||
@addAccelerators(item.submenu)
|
||||
|
||||
pruneRedundantSeparators: (menu) ->
|
||||
keepNextItemIfSeparator = false
|
||||
index = 0
|
||||
while index < menu.length
|
||||
if menu[index].type is 'separator'
|
||||
if not keepNextItemIfSeparator or index is menu.length - 1
|
||||
menu.splice(index, 1)
|
||||
else
|
||||
index++
|
||||
else
|
||||
keepNextItemIfSeparator = true
|
||||
index++
|
||||
|
||||
sortTemplate: (template) ->
|
||||
template = sortMenuItems(template)
|
||||
for id, item of template
|
||||
if Array.isArray(item.submenu)
|
||||
item.submenu = @sortTemplate(item.submenu)
|
||||
return template
|
||||
|
||||
# Returns an object compatible with `::add()` or `null`.
|
||||
cloneItemForEvent: (item, event) ->
|
||||
@@ -160,27 +196,6 @@ class ContextMenuManager
|
||||
.filter((submenuItem) -> submenuItem isnt null)
|
||||
return item
|
||||
|
||||
convertLegacyItemsBySelector: (legacyItemsBySelector, devMode) ->
|
||||
itemsBySelector = {}
|
||||
|
||||
for selector, commandsByLabel of legacyItemsBySelector
|
||||
itemsBySelector[selector] = @convertLegacyItems(commandsByLabel, devMode)
|
||||
|
||||
itemsBySelector
|
||||
|
||||
convertLegacyItems: (legacyItems, devMode) ->
|
||||
items = []
|
||||
|
||||
for label, commandOrSubmenu of legacyItems
|
||||
if typeof commandOrSubmenu is 'object'
|
||||
items.push({label, submenu: @convertLegacyItems(commandOrSubmenu, devMode), devMode})
|
||||
else if commandOrSubmenu is '-'
|
||||
items.push({type: 'separator'})
|
||||
else
|
||||
items.push({label, command: commandOrSubmenu, devMode})
|
||||
|
||||
items
|
||||
|
||||
showForEvent: (event) ->
|
||||
@activeElement = event.target
|
||||
menuTemplate = @templateForEvent(event)
|
||||
@@ -192,14 +207,17 @@ class ContextMenuManager
|
||||
clear: ->
|
||||
@activeElement = null
|
||||
@itemSets = []
|
||||
@add 'atom-workspace': [{
|
||||
label: 'Inspect Element'
|
||||
command: 'application:inspect'
|
||||
devMode: true
|
||||
created: (event) ->
|
||||
{pageX, pageY} = event
|
||||
@commandDetail = {x: pageX, y: pageY}
|
||||
}]
|
||||
inspectElement = {
|
||||
'atom-workspace': [{
|
||||
label: 'Inspect Element'
|
||||
command: 'application:inspect'
|
||||
devMode: true
|
||||
created: (event) ->
|
||||
{pageX, pageY} = event
|
||||
@commandDetail = {x: pageX, y: pageY}
|
||||
}]
|
||||
}
|
||||
@add(inspectElement, false)
|
||||
|
||||
class ContextMenuItemSet
|
||||
constructor: (@selector, @items) ->
|
||||
|
||||
45
src/core-uri-handlers.js
Normal file
45
src/core-uri-handlers.js
Normal file
@@ -0,0 +1,45 @@
|
||||
// Converts a query string parameter for a line or column number
|
||||
// to a zero-based line or column number for the Atom API.
|
||||
function getLineColNumber (numStr) {
|
||||
const num = parseInt(numStr || 0, 10)
|
||||
return Math.max(num - 1, 0)
|
||||
}
|
||||
|
||||
function openFile (atom, {query}) {
|
||||
const {filename, line, column} = query
|
||||
|
||||
atom.workspace.open(filename, {
|
||||
initialLine: getLineColNumber(line),
|
||||
initialColumn: getLineColNumber(column),
|
||||
searchAllPanes: true
|
||||
})
|
||||
}
|
||||
|
||||
function windowShouldOpenFile ({query}) {
|
||||
const {filename} = query
|
||||
return (win) => win.containsPath(filename)
|
||||
}
|
||||
|
||||
const ROUTER = {
|
||||
'/open/file': { handler: openFile, getWindowPredicate: windowShouldOpenFile }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create (atomEnv) {
|
||||
return function coreURIHandler (parsed) {
|
||||
const config = ROUTER[parsed.pathname]
|
||||
if (config) {
|
||||
config.handler(atomEnv, parsed)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
windowPredicate (parsed) {
|
||||
const config = ROUTER[parsed.pathname]
|
||||
if (config && config.getWindowPredicate) {
|
||||
return config.getWindowPredicate(parsed)
|
||||
} else {
|
||||
return (win) => true
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/crash-reporter-start.js
Normal file
10
src/crash-reporter-start.js
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = function (extra) {
|
||||
const {crashReporter} = require('electron')
|
||||
crashReporter.start({
|
||||
productName: 'Atom',
|
||||
companyName: 'GitHub',
|
||||
submitURL: 'https://crashreporter.atom.io',
|
||||
uploadToServer: false,
|
||||
extra: extra
|
||||
})
|
||||
}
|
||||
@@ -1,688 +0,0 @@
|
||||
{Point, Range} = require 'text-buffer'
|
||||
{Emitter} = require 'event-kit'
|
||||
_ = require 'underscore-plus'
|
||||
Model = require './model'
|
||||
|
||||
EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g
|
||||
|
||||
# Extended: The `Cursor` class represents the little blinking line identifying
|
||||
# where text can be inserted.
|
||||
#
|
||||
# Cursors belong to {TextEditor}s and have some metadata attached in the form
|
||||
# of a {TextEditorMarker}.
|
||||
module.exports =
|
||||
class Cursor extends Model
|
||||
screenPosition: null
|
||||
bufferPosition: null
|
||||
goalColumn: null
|
||||
visible: true
|
||||
|
||||
# Instantiated by a {TextEditor}
|
||||
constructor: ({@editor, @marker, @config, id}) ->
|
||||
@emitter = new Emitter
|
||||
|
||||
@assignId(id)
|
||||
@updateVisibility()
|
||||
|
||||
destroy: ->
|
||||
@marker.destroy()
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
###
|
||||
|
||||
# Public: Calls your `callback` when the cursor has been moved.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
# * `event` {Object}
|
||||
# * `oldBufferPosition` {Point}
|
||||
# * `oldScreenPosition` {Point}
|
||||
# * `newBufferPosition` {Point}
|
||||
# * `newScreenPosition` {Point}
|
||||
# * `textChanged` {Boolean}
|
||||
# * `Cursor` {Cursor} that triggered the event
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangePosition: (callback) ->
|
||||
@emitter.on 'did-change-position', callback
|
||||
|
||||
# Public: Calls your `callback` when the cursor is destroyed
|
||||
#
|
||||
# * `callback` {Function}
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy: (callback) ->
|
||||
@emitter.on 'did-destroy', callback
|
||||
|
||||
# Public: Calls your `callback` when the cursor's visibility has changed
|
||||
#
|
||||
# * `callback` {Function}
|
||||
# * `visibility` {Boolean}
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeVisibility: (callback) ->
|
||||
@emitter.on 'did-change-visibility', callback
|
||||
|
||||
###
|
||||
Section: Managing Cursor Position
|
||||
###
|
||||
|
||||
# Public: Moves a cursor to a given screen position.
|
||||
#
|
||||
# * `screenPosition` {Array} of two numbers: the screen row, and the screen column.
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `autoscroll` A Boolean which, if `true`, scrolls the {TextEditor} to wherever
|
||||
# the cursor moves to.
|
||||
setScreenPosition: (screenPosition, options={}) ->
|
||||
@changePosition options, =>
|
||||
@marker.setHeadScreenPosition(screenPosition, options)
|
||||
|
||||
# Public: Returns the screen position of the cursor as a {Point}.
|
||||
getScreenPosition: ->
|
||||
@marker.getHeadScreenPosition()
|
||||
|
||||
# Public: Moves a cursor to a given buffer position.
|
||||
#
|
||||
# * `bufferPosition` {Array} of two numbers: the buffer row, and the buffer column.
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `autoscroll` {Boolean} indicating whether to autoscroll to the new
|
||||
# position. Defaults to `true` if this is the most recently added cursor,
|
||||
# `false` otherwise.
|
||||
setBufferPosition: (bufferPosition, options={}) ->
|
||||
@changePosition options, =>
|
||||
@marker.setHeadBufferPosition(bufferPosition, options)
|
||||
|
||||
# Public: Returns the current buffer position as an Array.
|
||||
getBufferPosition: ->
|
||||
@marker.getHeadBufferPosition()
|
||||
|
||||
# Public: Returns the cursor's current screen row.
|
||||
getScreenRow: ->
|
||||
@getScreenPosition().row
|
||||
|
||||
# Public: Returns the cursor's current screen column.
|
||||
getScreenColumn: ->
|
||||
@getScreenPosition().column
|
||||
|
||||
# Public: Retrieves the cursor's current buffer row.
|
||||
getBufferRow: ->
|
||||
@getBufferPosition().row
|
||||
|
||||
# Public: Returns the cursor's current buffer column.
|
||||
getBufferColumn: ->
|
||||
@getBufferPosition().column
|
||||
|
||||
# Public: Returns the cursor's current buffer row of text excluding its line
|
||||
# ending.
|
||||
getCurrentBufferLine: ->
|
||||
@editor.lineTextForBufferRow(@getBufferRow())
|
||||
|
||||
# Public: Returns whether the cursor is at the start of a line.
|
||||
isAtBeginningOfLine: ->
|
||||
@getBufferPosition().column is 0
|
||||
|
||||
# Public: Returns whether the cursor is on the line return character.
|
||||
isAtEndOfLine: ->
|
||||
@getBufferPosition().isEqual(@getCurrentLineBufferRange().end)
|
||||
|
||||
###
|
||||
Section: Cursor Position Details
|
||||
###
|
||||
|
||||
# Public: Returns the underlying {TextEditorMarker} for the cursor.
|
||||
# Useful with overlay {Decoration}s.
|
||||
getMarker: -> @marker
|
||||
|
||||
# Public: Identifies if the cursor is surrounded by whitespace.
|
||||
#
|
||||
# "Surrounded" here means that the character directly before and after the
|
||||
# cursor are both whitespace.
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
isSurroundedByWhitespace: ->
|
||||
{row, column} = @getBufferPosition()
|
||||
range = [[row, column - 1], [row, column + 1]]
|
||||
/^\s+$/.test @editor.getTextInBufferRange(range)
|
||||
|
||||
# Public: Returns whether the cursor is currently between a word and non-word
|
||||
# character. The non-word characters are defined by the
|
||||
# `editor.nonWordCharacters` config value.
|
||||
#
|
||||
# This method returns false if the character before or after the cursor is
|
||||
# whitespace.
|
||||
#
|
||||
# Returns a Boolean.
|
||||
isBetweenWordAndNonWord: ->
|
||||
return false if @isAtBeginningOfLine() or @isAtEndOfLine()
|
||||
|
||||
{row, column} = @getBufferPosition()
|
||||
range = [[row, column - 1], [row, column + 1]]
|
||||
[before, after] = @editor.getTextInBufferRange(range)
|
||||
return false if /\s/.test(before) or /\s/.test(after)
|
||||
|
||||
nonWordCharacters = @config.get('editor.nonWordCharacters', scope: @getScopeDescriptor()).split('')
|
||||
_.contains(nonWordCharacters, before) isnt _.contains(nonWordCharacters, after)
|
||||
|
||||
# Public: Returns whether this cursor is between a word's start and end.
|
||||
#
|
||||
# * `options` (optional) {Object}
|
||||
# * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
# (default: {::wordRegExp}).
|
||||
#
|
||||
# Returns a {Boolean}
|
||||
isInsideWord: (options) ->
|
||||
{row, column} = @getBufferPosition()
|
||||
range = [[row, column], [row, Infinity]]
|
||||
@editor.getTextInBufferRange(range).search(options?.wordRegex ? @wordRegExp()) is 0
|
||||
|
||||
# Public: Returns the indentation level of the current line.
|
||||
getIndentLevel: ->
|
||||
if @editor.getSoftTabs()
|
||||
@getBufferColumn() / @editor.getTabLength()
|
||||
else
|
||||
@getBufferColumn()
|
||||
|
||||
# Public: Retrieves the scope descriptor for the cursor's current position.
|
||||
#
|
||||
# Returns a {ScopeDescriptor}
|
||||
getScopeDescriptor: ->
|
||||
@editor.scopeDescriptorForBufferPosition(@getBufferPosition())
|
||||
|
||||
# Public: Returns true if this cursor has no non-whitespace characters before
|
||||
# its current position.
|
||||
hasPrecedingCharactersOnLine: ->
|
||||
bufferPosition = @getBufferPosition()
|
||||
line = @editor.lineTextForBufferRow(bufferPosition.row)
|
||||
firstCharacterColumn = line.search(/\S/)
|
||||
|
||||
if firstCharacterColumn is -1
|
||||
false
|
||||
else
|
||||
bufferPosition.column > firstCharacterColumn
|
||||
|
||||
# Public: Identifies if this cursor is the last in the {TextEditor}.
|
||||
#
|
||||
# "Last" is defined as the most recently added cursor.
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
isLastCursor: ->
|
||||
this is @editor.getLastCursor()
|
||||
|
||||
###
|
||||
Section: Moving the Cursor
|
||||
###
|
||||
|
||||
# Public: Moves the cursor up one screen row.
|
||||
#
|
||||
# * `rowCount` (optional) {Number} number of rows to move (default: 1)
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `moveToEndOfSelection` if true, move to the left of the selection if a
|
||||
# selection exists.
|
||||
moveUp: (rowCount=1, {moveToEndOfSelection}={}) ->
|
||||
range = @marker.getScreenRange()
|
||||
if moveToEndOfSelection and not range.isEmpty()
|
||||
{row, column} = range.start
|
||||
else
|
||||
{row, column} = @getScreenPosition()
|
||||
|
||||
column = @goalColumn if @goalColumn?
|
||||
@setScreenPosition({row: row - rowCount, column: column}, skipSoftWrapIndentation: true)
|
||||
@goalColumn = column
|
||||
|
||||
# Public: Moves the cursor down one screen row.
|
||||
#
|
||||
# * `rowCount` (optional) {Number} number of rows to move (default: 1)
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `moveToEndOfSelection` if true, move to the left of the selection if a
|
||||
# selection exists.
|
||||
moveDown: (rowCount=1, {moveToEndOfSelection}={}) ->
|
||||
range = @marker.getScreenRange()
|
||||
if moveToEndOfSelection and not range.isEmpty()
|
||||
{row, column} = range.end
|
||||
else
|
||||
{row, column} = @getScreenPosition()
|
||||
|
||||
column = @goalColumn if @goalColumn?
|
||||
@setScreenPosition({row: row + rowCount, column: column}, skipSoftWrapIndentation: true)
|
||||
@goalColumn = column
|
||||
|
||||
# Public: Moves the cursor left one screen column.
|
||||
#
|
||||
# * `columnCount` (optional) {Number} number of columns to move (default: 1)
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `moveToEndOfSelection` if true, move to the left of the selection if a
|
||||
# selection exists.
|
||||
moveLeft: (columnCount=1, {moveToEndOfSelection}={}) ->
|
||||
range = @marker.getScreenRange()
|
||||
if moveToEndOfSelection and not range.isEmpty()
|
||||
@setScreenPosition(range.start)
|
||||
else
|
||||
{row, column} = @getScreenPosition()
|
||||
|
||||
while columnCount > column and row > 0
|
||||
columnCount -= column
|
||||
column = @editor.lineTextForScreenRow(--row).length
|
||||
columnCount-- # subtract 1 for the row move
|
||||
|
||||
column = column - columnCount
|
||||
@setScreenPosition({row, column}, clip: 'backward')
|
||||
|
||||
# Public: Moves the cursor right one screen column.
|
||||
#
|
||||
# * `columnCount` (optional) {Number} number of columns to move (default: 1)
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `moveToEndOfSelection` if true, move to the right of the selection if a
|
||||
# selection exists.
|
||||
moveRight: (columnCount=1, {moveToEndOfSelection}={}) ->
|
||||
range = @marker.getScreenRange()
|
||||
if moveToEndOfSelection and not range.isEmpty()
|
||||
@setScreenPosition(range.end)
|
||||
else
|
||||
{row, column} = @getScreenPosition()
|
||||
maxLines = @editor.getScreenLineCount()
|
||||
rowLength = @editor.lineTextForScreenRow(row).length
|
||||
columnsRemainingInLine = rowLength - column
|
||||
|
||||
while columnCount > columnsRemainingInLine and row < maxLines - 1
|
||||
columnCount -= columnsRemainingInLine
|
||||
columnCount-- # subtract 1 for the row move
|
||||
|
||||
column = 0
|
||||
rowLength = @editor.lineTextForScreenRow(++row).length
|
||||
columnsRemainingInLine = rowLength
|
||||
|
||||
column = column + columnCount
|
||||
@setScreenPosition({row, column}, clip: 'forward', wrapBeyondNewlines: true, wrapAtSoftNewlines: true)
|
||||
|
||||
# Public: Moves the cursor to the top of the buffer.
|
||||
moveToTop: ->
|
||||
@setBufferPosition([0, 0])
|
||||
|
||||
# Public: Moves the cursor to the bottom of the buffer.
|
||||
moveToBottom: ->
|
||||
@setBufferPosition(@editor.getEofBufferPosition())
|
||||
|
||||
# Public: Moves the cursor to the beginning of the line.
|
||||
moveToBeginningOfScreenLine: ->
|
||||
@setScreenPosition([@getScreenRow(), 0])
|
||||
|
||||
# Public: Moves the cursor to the beginning of the buffer line.
|
||||
moveToBeginningOfLine: ->
|
||||
@setBufferPosition([@getBufferRow(), 0])
|
||||
|
||||
# Public: Moves the cursor to the beginning of the first character in the
|
||||
# line.
|
||||
moveToFirstCharacterOfLine: ->
|
||||
screenRow = @getScreenRow()
|
||||
screenLineStart = @editor.clipScreenPosition([screenRow, 0], skipSoftWrapIndentation: true)
|
||||
screenLineEnd = [screenRow, Infinity]
|
||||
screenLineBufferRange = @editor.bufferRangeForScreenRange([screenLineStart, screenLineEnd])
|
||||
|
||||
firstCharacterColumn = null
|
||||
@editor.scanInBufferRange /\S/, screenLineBufferRange, ({range, stop}) ->
|
||||
firstCharacterColumn = range.start.column
|
||||
stop()
|
||||
|
||||
if firstCharacterColumn? and firstCharacterColumn isnt @getBufferColumn()
|
||||
targetBufferColumn = firstCharacterColumn
|
||||
else
|
||||
targetBufferColumn = screenLineBufferRange.start.column
|
||||
|
||||
@setBufferPosition([screenLineBufferRange.start.row, targetBufferColumn])
|
||||
|
||||
# Public: Moves the cursor to the end of the line.
|
||||
moveToEndOfScreenLine: ->
|
||||
@setScreenPosition([@getScreenRow(), Infinity])
|
||||
|
||||
# Public: Moves the cursor to the end of the buffer line.
|
||||
moveToEndOfLine: ->
|
||||
@setBufferPosition([@getBufferRow(), Infinity])
|
||||
|
||||
# Public: Moves the cursor to the beginning of the word.
|
||||
moveToBeginningOfWord: ->
|
||||
@setBufferPosition(@getBeginningOfCurrentWordBufferPosition())
|
||||
|
||||
# Public: Moves the cursor to the end of the word.
|
||||
moveToEndOfWord: ->
|
||||
if position = @getEndOfCurrentWordBufferPosition()
|
||||
@setBufferPosition(position)
|
||||
|
||||
# Public: Moves the cursor to the beginning of the next word.
|
||||
moveToBeginningOfNextWord: ->
|
||||
if position = @getBeginningOfNextWordBufferPosition()
|
||||
@setBufferPosition(position)
|
||||
|
||||
# Public: Moves the cursor to the previous word boundary.
|
||||
moveToPreviousWordBoundary: ->
|
||||
if position = @getPreviousWordBoundaryBufferPosition()
|
||||
@setBufferPosition(position)
|
||||
|
||||
# Public: Moves the cursor to the next word boundary.
|
||||
moveToNextWordBoundary: ->
|
||||
if position = @getNextWordBoundaryBufferPosition()
|
||||
@setBufferPosition(position)
|
||||
|
||||
# Public: Moves the cursor to the previous subword boundary.
|
||||
moveToPreviousSubwordBoundary: ->
|
||||
options = {wordRegex: @subwordRegExp(backwards: true)}
|
||||
if position = @getPreviousWordBoundaryBufferPosition(options)
|
||||
@setBufferPosition(position)
|
||||
|
||||
# Public: Moves the cursor to the next subword boundary.
|
||||
moveToNextSubwordBoundary: ->
|
||||
options = {wordRegex: @subwordRegExp()}
|
||||
if position = @getNextWordBoundaryBufferPosition(options)
|
||||
@setBufferPosition(position)
|
||||
|
||||
# Public: Moves the cursor to the beginning of the buffer line, skipping all
|
||||
# whitespace.
|
||||
skipLeadingWhitespace: ->
|
||||
position = @getBufferPosition()
|
||||
scanRange = @getCurrentLineBufferRange()
|
||||
endOfLeadingWhitespace = null
|
||||
@editor.scanInBufferRange /^[ \t]*/, scanRange, ({range}) ->
|
||||
endOfLeadingWhitespace = range.end
|
||||
|
||||
@setBufferPosition(endOfLeadingWhitespace) if endOfLeadingWhitespace.isGreaterThan(position)
|
||||
|
||||
# Public: Moves the cursor to the beginning of the next paragraph
|
||||
moveToBeginningOfNextParagraph: ->
|
||||
if position = @getBeginningOfNextParagraphBufferPosition()
|
||||
@setBufferPosition(position)
|
||||
|
||||
# Public: Moves the cursor to the beginning of the previous paragraph
|
||||
moveToBeginningOfPreviousParagraph: ->
|
||||
if position = @getBeginningOfPreviousParagraphBufferPosition()
|
||||
@setBufferPosition(position)
|
||||
|
||||
###
|
||||
Section: Local Positions and Ranges
|
||||
###
|
||||
|
||||
# Public: Returns buffer position of previous word boundary. It might be on
|
||||
# the current word, or the previous word.
|
||||
#
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
# (default: {::wordRegExp})
|
||||
getPreviousWordBoundaryBufferPosition: (options = {}) ->
|
||||
currentBufferPosition = @getBufferPosition()
|
||||
previousNonBlankRow = @editor.buffer.previousNonBlankRow(currentBufferPosition.row)
|
||||
scanRange = [[previousNonBlankRow ? 0, 0], currentBufferPosition]
|
||||
|
||||
beginningOfWordPosition = null
|
||||
@editor.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) ->
|
||||
if range.start.row < currentBufferPosition.row and currentBufferPosition.column > 0
|
||||
# force it to stop at the beginning of each line
|
||||
beginningOfWordPosition = new Point(currentBufferPosition.row, 0)
|
||||
else if range.end.isLessThan(currentBufferPosition)
|
||||
beginningOfWordPosition = range.end
|
||||
else
|
||||
beginningOfWordPosition = range.start
|
||||
|
||||
if not beginningOfWordPosition?.isEqual(currentBufferPosition)
|
||||
stop()
|
||||
|
||||
beginningOfWordPosition or currentBufferPosition
|
||||
|
||||
# Public: Returns buffer position of the next word boundary. It might be on
|
||||
# the current word, or the previous word.
|
||||
#
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
# (default: {::wordRegExp})
|
||||
getNextWordBoundaryBufferPosition: (options = {}) ->
|
||||
currentBufferPosition = @getBufferPosition()
|
||||
scanRange = [currentBufferPosition, @editor.getEofBufferPosition()]
|
||||
|
||||
endOfWordPosition = null
|
||||
@editor.scanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) ->
|
||||
if range.start.row > currentBufferPosition.row
|
||||
# force it to stop at the beginning of each line
|
||||
endOfWordPosition = new Point(range.start.row, 0)
|
||||
else if range.start.isGreaterThan(currentBufferPosition)
|
||||
endOfWordPosition = range.start
|
||||
else
|
||||
endOfWordPosition = range.end
|
||||
|
||||
if not endOfWordPosition?.isEqual(currentBufferPosition)
|
||||
stop()
|
||||
|
||||
endOfWordPosition or currentBufferPosition
|
||||
|
||||
# Public: Retrieves the buffer position of where the current word starts.
|
||||
#
|
||||
# * `options` (optional) An {Object} with the following keys:
|
||||
# * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
# (default: {::wordRegExp}).
|
||||
# * `includeNonWordCharacters` A {Boolean} indicating whether to include
|
||||
# non-word characters in the default word regex.
|
||||
# Has no effect if wordRegex is set.
|
||||
# * `allowPrevious` A {Boolean} indicating whether the beginning of the
|
||||
# previous word can be returned.
|
||||
#
|
||||
# Returns a {Range}.
|
||||
getBeginningOfCurrentWordBufferPosition: (options = {}) ->
|
||||
allowPrevious = options.allowPrevious ? true
|
||||
currentBufferPosition = @getBufferPosition()
|
||||
previousNonBlankRow = @editor.buffer.previousNonBlankRow(currentBufferPosition.row) ? 0
|
||||
scanRange = [[previousNonBlankRow, 0], currentBufferPosition]
|
||||
|
||||
beginningOfWordPosition = null
|
||||
@editor.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, matchText, stop}) ->
|
||||
# Ignore 'empty line' matches between '\r' and '\n'
|
||||
return if matchText is '' and range.start.column isnt 0
|
||||
|
||||
if range.start.isLessThan(currentBufferPosition)
|
||||
if range.end.isGreaterThanOrEqual(currentBufferPosition) or allowPrevious
|
||||
beginningOfWordPosition = range.start
|
||||
stop()
|
||||
|
||||
if beginningOfWordPosition?
|
||||
beginningOfWordPosition
|
||||
else if allowPrevious
|
||||
new Point(0, 0)
|
||||
else
|
||||
currentBufferPosition
|
||||
|
||||
# Public: Retrieves the buffer position of where the current word ends.
|
||||
#
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
# (default: {::wordRegExp})
|
||||
# * `includeNonWordCharacters` A Boolean indicating whether to include
|
||||
# non-word characters in the default word regex. Has no effect if
|
||||
# wordRegex is set.
|
||||
#
|
||||
# Returns a {Range}.
|
||||
getEndOfCurrentWordBufferPosition: (options = {}) ->
|
||||
allowNext = options.allowNext ? true
|
||||
currentBufferPosition = @getBufferPosition()
|
||||
scanRange = [currentBufferPosition, @editor.getEofBufferPosition()]
|
||||
|
||||
endOfWordPosition = null
|
||||
@editor.scanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, matchText, stop}) ->
|
||||
# Ignore 'empty line' matches between '\r' and '\n'
|
||||
return if matchText is '' and range.start.column isnt 0
|
||||
|
||||
if range.end.isGreaterThan(currentBufferPosition)
|
||||
if allowNext or range.start.isLessThanOrEqual(currentBufferPosition)
|
||||
endOfWordPosition = range.end
|
||||
stop()
|
||||
|
||||
endOfWordPosition ? currentBufferPosition
|
||||
|
||||
# Public: Retrieves the buffer position of where the next word starts.
|
||||
#
|
||||
# * `options` (optional) {Object}
|
||||
# * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
# (default: {::wordRegExp}).
|
||||
#
|
||||
# Returns a {Range}
|
||||
getBeginningOfNextWordBufferPosition: (options = {}) ->
|
||||
currentBufferPosition = @getBufferPosition()
|
||||
start = if @isInsideWord(options) then @getEndOfCurrentWordBufferPosition(options) else currentBufferPosition
|
||||
scanRange = [start, @editor.getEofBufferPosition()]
|
||||
|
||||
beginningOfNextWordPosition = null
|
||||
@editor.scanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) ->
|
||||
beginningOfNextWordPosition = range.start
|
||||
stop()
|
||||
|
||||
beginningOfNextWordPosition or currentBufferPosition
|
||||
|
||||
# Public: Returns the buffer Range occupied by the word located under the cursor.
|
||||
#
|
||||
# * `options` (optional) {Object}
|
||||
# * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
# (default: {::wordRegExp}).
|
||||
getCurrentWordBufferRange: (options={}) ->
|
||||
startOptions = _.extend(_.clone(options), allowPrevious: false)
|
||||
endOptions = _.extend(_.clone(options), allowNext: false)
|
||||
new Range(@getBeginningOfCurrentWordBufferPosition(startOptions), @getEndOfCurrentWordBufferPosition(endOptions))
|
||||
|
||||
# Public: Returns the buffer Range for the current line.
|
||||
#
|
||||
# * `options` (optional) {Object}
|
||||
# * `includeNewline` A {Boolean} which controls whether the Range should
|
||||
# include the newline.
|
||||
getCurrentLineBufferRange: (options) ->
|
||||
@editor.bufferRangeForBufferRow(@getBufferRow(), options)
|
||||
|
||||
# Public: Retrieves the range for the current paragraph.
|
||||
#
|
||||
# A paragraph is defined as a block of text surrounded by empty lines or comments.
|
||||
#
|
||||
# Returns a {Range}.
|
||||
getCurrentParagraphBufferRange: ->
|
||||
@editor.languageMode.rowRangeForParagraphAtBufferRow(@getBufferRow())
|
||||
|
||||
# Public: Returns the characters preceding the cursor in the current word.
|
||||
getCurrentWordPrefix: ->
|
||||
@editor.getTextInBufferRange([@getBeginningOfCurrentWordBufferPosition(), @getBufferPosition()])
|
||||
|
||||
###
|
||||
Section: Visibility
|
||||
###
|
||||
|
||||
# Public: Sets whether the cursor is visible.
|
||||
setVisible: (visible) ->
|
||||
if @visible isnt visible
|
||||
@visible = visible
|
||||
@emitter.emit 'did-change-visibility', @visible
|
||||
|
||||
# Public: Returns the visibility of the cursor.
|
||||
isVisible: -> @visible
|
||||
|
||||
updateVisibility: ->
|
||||
@setVisible(@marker.getBufferRange().isEmpty())
|
||||
|
||||
###
|
||||
Section: Comparing to another cursor
|
||||
###
|
||||
|
||||
# Public: Compare this cursor's buffer position to another cursor's buffer position.
|
||||
#
|
||||
# See {Point::compare} for more details.
|
||||
#
|
||||
# * `otherCursor`{Cursor} to compare against
|
||||
compare: (otherCursor) ->
|
||||
@getBufferPosition().compare(otherCursor.getBufferPosition())
|
||||
|
||||
###
|
||||
Section: Utilities
|
||||
###
|
||||
|
||||
# Public: Prevents this cursor from causing scrolling.
|
||||
clearAutoscroll: ->
|
||||
|
||||
# Public: Deselects the current selection.
|
||||
clearSelection: (options) ->
|
||||
@selection?.clear(options)
|
||||
|
||||
# Public: Get the RegExp used by the cursor to determine what a "word" is.
|
||||
#
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `includeNonWordCharacters` A {Boolean} indicating whether to include
|
||||
# non-word characters in the regex. (default: true)
|
||||
#
|
||||
# Returns a {RegExp}.
|
||||
wordRegExp: (options) ->
|
||||
scope = @getScopeDescriptor()
|
||||
nonWordCharacters = _.escapeRegExp(@config.get('editor.nonWordCharacters', {scope}))
|
||||
|
||||
source = "^[\t ]*$|[^\\s#{nonWordCharacters}]+"
|
||||
if options?.includeNonWordCharacters ? true
|
||||
source += "|" + "[#{nonWordCharacters}]+"
|
||||
new RegExp(source, "g")
|
||||
|
||||
# Public: Get the RegExp used by the cursor to determine what a "subword" is.
|
||||
#
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `backwards` A {Boolean} indicating whether to look forwards or backwards
|
||||
# for the next subword. (default: false)
|
||||
#
|
||||
# Returns a {RegExp}.
|
||||
subwordRegExp: (options={}) ->
|
||||
nonWordCharacters = @config.get('editor.nonWordCharacters', scope: @getScopeDescriptor())
|
||||
lowercaseLetters = 'a-z\\u00DF-\\u00F6\\u00F8-\\u00FF'
|
||||
uppercaseLetters = 'A-Z\\u00C0-\\u00D6\\u00D8-\\u00DE'
|
||||
snakeCamelSegment = "[#{uppercaseLetters}]?[#{lowercaseLetters}]+"
|
||||
segments = [
|
||||
"^[\t ]+",
|
||||
"[\t ]+$",
|
||||
"[#{uppercaseLetters}]+(?![#{lowercaseLetters}])",
|
||||
"\\d+"
|
||||
]
|
||||
if options.backwards
|
||||
segments.push("#{snakeCamelSegment}_*")
|
||||
segments.push("[#{_.escapeRegExp(nonWordCharacters)}]+\\s*")
|
||||
else
|
||||
segments.push("_*#{snakeCamelSegment}")
|
||||
segments.push("\\s*[#{_.escapeRegExp(nonWordCharacters)}]+")
|
||||
segments.push("_+")
|
||||
new RegExp(segments.join("|"), "g")
|
||||
|
||||
###
|
||||
Section: Private
|
||||
###
|
||||
|
||||
changePosition: (options, fn) ->
|
||||
@clearSelection(autoscroll: false)
|
||||
fn()
|
||||
@autoscroll() if options.autoscroll ? @isLastCursor()
|
||||
|
||||
getPixelRect: ->
|
||||
@editor.pixelRectForScreenRange(@getScreenRange())
|
||||
|
||||
getScreenRange: ->
|
||||
{row, column} = @getScreenPosition()
|
||||
new Range(new Point(row, column), new Point(row, column + 1))
|
||||
|
||||
autoscroll: (options) ->
|
||||
@editor.scrollToScreenRange(@getScreenRange(), options)
|
||||
|
||||
getBeginningOfNextParagraphBufferPosition: ->
|
||||
start = @getBufferPosition()
|
||||
eof = @editor.getEofBufferPosition()
|
||||
scanRange = [start, eof]
|
||||
|
||||
{row, column} = eof
|
||||
position = new Point(row, column - 1)
|
||||
|
||||
@editor.scanInBufferRange EmptyLineRegExp, scanRange, ({range, stop}) ->
|
||||
position = range.start.traverse(Point(1, 0))
|
||||
stop() unless position.isEqual(start)
|
||||
position
|
||||
|
||||
getBeginningOfPreviousParagraphBufferPosition: ->
|
||||
start = @getBufferPosition()
|
||||
|
||||
{row, column} = start
|
||||
scanRange = [[row-1, column], [0, 0]]
|
||||
position = new Point(0, 0)
|
||||
zero = new Point(0, 0)
|
||||
@editor.backwardsScanInBufferRange EmptyLineRegExp, scanRange, ({range, stop}) ->
|
||||
position = range.start.traverse(Point(1, 0))
|
||||
stop() unless position.isEqual(start)
|
||||
position
|
||||
760
src/cursor.js
Normal file
760
src/cursor.js
Normal file
@@ -0,0 +1,760 @@
|
||||
const {Point, Range} = require('text-buffer')
|
||||
const {Emitter} = require('event-kit')
|
||||
const _ = require('underscore-plus')
|
||||
const Model = require('./model')
|
||||
|
||||
const EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g
|
||||
|
||||
// Extended: The `Cursor` class represents the little blinking line identifying
|
||||
// where text can be inserted.
|
||||
//
|
||||
// Cursors belong to {TextEditor}s and have some metadata attached in the form
|
||||
// of a {DisplayMarker}.
|
||||
module.exports =
|
||||
class Cursor extends Model {
|
||||
// Instantiated by a {TextEditor}
|
||||
constructor (params) {
|
||||
super(params)
|
||||
this.editor = params.editor
|
||||
this.marker = params.marker
|
||||
this.emitter = new Emitter()
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.marker.destroy()
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Event Subscription
|
||||
*/
|
||||
|
||||
// Public: Calls your `callback` when the cursor has been moved.
|
||||
//
|
||||
// * `callback` {Function}
|
||||
// * `event` {Object}
|
||||
// * `oldBufferPosition` {Point}
|
||||
// * `oldScreenPosition` {Point}
|
||||
// * `newBufferPosition` {Point}
|
||||
// * `newScreenPosition` {Point}
|
||||
// * `textChanged` {Boolean}
|
||||
// * `cursor` {Cursor} that triggered the event
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangePosition (callback) {
|
||||
return this.emitter.on('did-change-position', callback)
|
||||
}
|
||||
|
||||
// Public: Calls your `callback` when the cursor is destroyed
|
||||
//
|
||||
// * `callback` {Function}
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy (callback) {
|
||||
return this.emitter.once('did-destroy', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Managing Cursor Position
|
||||
*/
|
||||
|
||||
// Public: Moves a cursor to a given screen position.
|
||||
//
|
||||
// * `screenPosition` {Array} of two numbers: the screen row, and the screen column.
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `autoscroll` A Boolean which, if `true`, scrolls the {TextEditor} to wherever
|
||||
// the cursor moves to.
|
||||
setScreenPosition (screenPosition, options = {}) {
|
||||
this.changePosition(options, () => {
|
||||
this.marker.setHeadScreenPosition(screenPosition, options)
|
||||
})
|
||||
}
|
||||
|
||||
// Public: Returns the screen position of the cursor as a {Point}.
|
||||
getScreenPosition () {
|
||||
return this.marker.getHeadScreenPosition()
|
||||
}
|
||||
|
||||
// Public: Moves a cursor to a given buffer position.
|
||||
//
|
||||
// * `bufferPosition` {Array} of two numbers: the buffer row, and the buffer column.
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `autoscroll` {Boolean} indicating whether to autoscroll to the new
|
||||
// position. Defaults to `true` if this is the most recently added cursor,
|
||||
// `false` otherwise.
|
||||
setBufferPosition (bufferPosition, options = {}) {
|
||||
this.changePosition(options, () => {
|
||||
this.marker.setHeadBufferPosition(bufferPosition, options)
|
||||
})
|
||||
}
|
||||
|
||||
// Public: Returns the current buffer position as an Array.
|
||||
getBufferPosition () {
|
||||
return this.marker.getHeadBufferPosition()
|
||||
}
|
||||
|
||||
// Public: Returns the cursor's current screen row.
|
||||
getScreenRow () {
|
||||
return this.getScreenPosition().row
|
||||
}
|
||||
|
||||
// Public: Returns the cursor's current screen column.
|
||||
getScreenColumn () {
|
||||
return this.getScreenPosition().column
|
||||
}
|
||||
|
||||
// Public: Retrieves the cursor's current buffer row.
|
||||
getBufferRow () {
|
||||
return this.getBufferPosition().row
|
||||
}
|
||||
|
||||
// Public: Returns the cursor's current buffer column.
|
||||
getBufferColumn () {
|
||||
return this.getBufferPosition().column
|
||||
}
|
||||
|
||||
// Public: Returns the cursor's current buffer row of text excluding its line
|
||||
// ending.
|
||||
getCurrentBufferLine () {
|
||||
return this.editor.lineTextForBufferRow(this.getBufferRow())
|
||||
}
|
||||
|
||||
// Public: Returns whether the cursor is at the start of a line.
|
||||
isAtBeginningOfLine () {
|
||||
return this.getBufferPosition().column === 0
|
||||
}
|
||||
|
||||
// Public: Returns whether the cursor is on the line return character.
|
||||
isAtEndOfLine () {
|
||||
return this.getBufferPosition().isEqual(this.getCurrentLineBufferRange().end)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Cursor Position Details
|
||||
*/
|
||||
|
||||
// Public: Returns the underlying {DisplayMarker} for the cursor.
|
||||
// Useful with overlay {Decoration}s.
|
||||
getMarker () { return this.marker }
|
||||
|
||||
// Public: Identifies if the cursor is surrounded by whitespace.
|
||||
//
|
||||
// "Surrounded" here means that the character directly before and after the
|
||||
// cursor are both whitespace.
|
||||
//
|
||||
// Returns a {Boolean}.
|
||||
isSurroundedByWhitespace () {
|
||||
const {row, column} = this.getBufferPosition()
|
||||
const range = [[row, column - 1], [row, column + 1]]
|
||||
return /^\s+$/.test(this.editor.getTextInBufferRange(range))
|
||||
}
|
||||
|
||||
// Public: Returns whether the cursor is currently between a word and non-word
|
||||
// character. The non-word characters are defined by the
|
||||
// `editor.nonWordCharacters` config value.
|
||||
//
|
||||
// This method returns false if the character before or after the cursor is
|
||||
// whitespace.
|
||||
//
|
||||
// Returns a Boolean.
|
||||
isBetweenWordAndNonWord () {
|
||||
if (this.isAtBeginningOfLine() || this.isAtEndOfLine()) return false
|
||||
|
||||
const {row, column} = this.getBufferPosition()
|
||||
const range = [[row, column - 1], [row, column + 1]]
|
||||
const text = this.editor.getTextInBufferRange(range)
|
||||
if (/\s/.test(text[0]) || /\s/.test(text[1])) return false
|
||||
|
||||
const nonWordCharacters = this.getNonWordCharacters()
|
||||
return nonWordCharacters.includes(text[0]) !== nonWordCharacters.includes(text[1])
|
||||
}
|
||||
|
||||
// Public: Returns whether this cursor is between a word's start and end.
|
||||
//
|
||||
// * `options` (optional) {Object}
|
||||
// * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
// (default: {::wordRegExp}).
|
||||
//
|
||||
// Returns a {Boolean}
|
||||
isInsideWord (options) {
|
||||
const {row, column} = this.getBufferPosition()
|
||||
const range = [[row, column], [row, Infinity]]
|
||||
const text = this.editor.getTextInBufferRange(range)
|
||||
return text.search((options && options.wordRegex) || this.wordRegExp()) === 0
|
||||
}
|
||||
|
||||
// Public: Returns the indentation level of the current line.
|
||||
getIndentLevel () {
|
||||
if (this.editor.getSoftTabs()) {
|
||||
return this.getBufferColumn() / this.editor.getTabLength()
|
||||
} else {
|
||||
return this.getBufferColumn()
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Retrieves the scope descriptor for the cursor's current position.
|
||||
//
|
||||
// Returns a {ScopeDescriptor}
|
||||
getScopeDescriptor () {
|
||||
return this.editor.scopeDescriptorForBufferPosition(this.getBufferPosition())
|
||||
}
|
||||
|
||||
// Public: Returns true if this cursor has no non-whitespace characters before
|
||||
// its current position.
|
||||
hasPrecedingCharactersOnLine () {
|
||||
const bufferPosition = this.getBufferPosition()
|
||||
const line = this.editor.lineTextForBufferRow(bufferPosition.row)
|
||||
const firstCharacterColumn = line.search(/\S/)
|
||||
|
||||
if (firstCharacterColumn === -1) {
|
||||
return false
|
||||
} else {
|
||||
return bufferPosition.column > firstCharacterColumn
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Identifies if this cursor is the last in the {TextEditor}.
|
||||
//
|
||||
// "Last" is defined as the most recently added cursor.
|
||||
//
|
||||
// Returns a {Boolean}.
|
||||
isLastCursor () {
|
||||
return this === this.editor.getLastCursor()
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Moving the Cursor
|
||||
*/
|
||||
|
||||
// Public: Moves the cursor up one screen row.
|
||||
//
|
||||
// * `rowCount` (optional) {Number} number of rows to move (default: 1)
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `moveToEndOfSelection` if true, move to the left of the selection if a
|
||||
// selection exists.
|
||||
moveUp (rowCount = 1, {moveToEndOfSelection} = {}) {
|
||||
let row, column
|
||||
const range = this.marker.getScreenRange()
|
||||
if (moveToEndOfSelection && !range.isEmpty()) {
|
||||
({row, column} = range.start)
|
||||
} else {
|
||||
({row, column} = this.getScreenPosition())
|
||||
}
|
||||
|
||||
if (this.goalColumn != null) column = this.goalColumn
|
||||
this.setScreenPosition({row: row - rowCount, column}, {skipSoftWrapIndentation: true})
|
||||
this.goalColumn = column
|
||||
}
|
||||
|
||||
// Public: Moves the cursor down one screen row.
|
||||
//
|
||||
// * `rowCount` (optional) {Number} number of rows to move (default: 1)
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `moveToEndOfSelection` if true, move to the left of the selection if a
|
||||
// selection exists.
|
||||
moveDown (rowCount = 1, {moveToEndOfSelection} = {}) {
|
||||
let row, column
|
||||
const range = this.marker.getScreenRange()
|
||||
if (moveToEndOfSelection && !range.isEmpty()) {
|
||||
({row, column} = range.end)
|
||||
} else {
|
||||
({row, column} = this.getScreenPosition())
|
||||
}
|
||||
|
||||
if (this.goalColumn != null) column = this.goalColumn
|
||||
this.setScreenPosition({row: row + rowCount, column}, {skipSoftWrapIndentation: true})
|
||||
this.goalColumn = column
|
||||
}
|
||||
|
||||
// Public: Moves the cursor left one screen column.
|
||||
//
|
||||
// * `columnCount` (optional) {Number} number of columns to move (default: 1)
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `moveToEndOfSelection` if true, move to the left of the selection if a
|
||||
// selection exists.
|
||||
moveLeft (columnCount = 1, {moveToEndOfSelection} = {}) {
|
||||
const range = this.marker.getScreenRange()
|
||||
if (moveToEndOfSelection && !range.isEmpty()) {
|
||||
this.setScreenPosition(range.start)
|
||||
} else {
|
||||
let {row, column} = this.getScreenPosition()
|
||||
|
||||
while (columnCount > column && row > 0) {
|
||||
columnCount -= column
|
||||
column = this.editor.lineLengthForScreenRow(--row)
|
||||
columnCount-- // subtract 1 for the row move
|
||||
}
|
||||
|
||||
column = column - columnCount
|
||||
this.setScreenPosition({row, column}, {clipDirection: 'backward'})
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Moves the cursor right one screen column.
|
||||
//
|
||||
// * `columnCount` (optional) {Number} number of columns to move (default: 1)
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `moveToEndOfSelection` if true, move to the right of the selection if a
|
||||
// selection exists.
|
||||
moveRight (columnCount = 1, {moveToEndOfSelection} = {}) {
|
||||
const range = this.marker.getScreenRange()
|
||||
if (moveToEndOfSelection && !range.isEmpty()) {
|
||||
this.setScreenPosition(range.end)
|
||||
} else {
|
||||
let {row, column} = this.getScreenPosition()
|
||||
const maxLines = this.editor.getScreenLineCount()
|
||||
let rowLength = this.editor.lineLengthForScreenRow(row)
|
||||
let columnsRemainingInLine = rowLength - column
|
||||
|
||||
while (columnCount > columnsRemainingInLine && row < maxLines - 1) {
|
||||
columnCount -= columnsRemainingInLine
|
||||
columnCount-- // subtract 1 for the row move
|
||||
|
||||
column = 0
|
||||
rowLength = this.editor.lineLengthForScreenRow(++row)
|
||||
columnsRemainingInLine = rowLength
|
||||
}
|
||||
|
||||
column = column + columnCount
|
||||
this.setScreenPosition({row, column}, {clipDirection: 'forward'})
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the top of the buffer.
|
||||
moveToTop () {
|
||||
this.setBufferPosition([0, 0])
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the bottom of the buffer.
|
||||
moveToBottom () {
|
||||
const column = this.goalColumn
|
||||
this.setBufferPosition(this.editor.getEofBufferPosition())
|
||||
this.goalColumn = column
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the beginning of the line.
|
||||
moveToBeginningOfScreenLine () {
|
||||
this.setScreenPosition([this.getScreenRow(), 0])
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the beginning of the buffer line.
|
||||
moveToBeginningOfLine () {
|
||||
this.setBufferPosition([this.getBufferRow(), 0])
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the beginning of the first character in the
|
||||
// line.
|
||||
moveToFirstCharacterOfLine () {
|
||||
let targetBufferColumn
|
||||
const screenRow = this.getScreenRow()
|
||||
const screenLineStart = this.editor.clipScreenPosition([screenRow, 0], {skipSoftWrapIndentation: true})
|
||||
const screenLineEnd = [screenRow, Infinity]
|
||||
const screenLineBufferRange = this.editor.bufferRangeForScreenRange([screenLineStart, screenLineEnd])
|
||||
|
||||
let firstCharacterColumn = null
|
||||
this.editor.scanInBufferRange(/\S/, screenLineBufferRange, ({range, stop}) => {
|
||||
firstCharacterColumn = range.start.column
|
||||
stop()
|
||||
})
|
||||
|
||||
if (firstCharacterColumn != null && firstCharacterColumn !== this.getBufferColumn()) {
|
||||
targetBufferColumn = firstCharacterColumn
|
||||
} else {
|
||||
targetBufferColumn = screenLineBufferRange.start.column
|
||||
}
|
||||
|
||||
this.setBufferPosition([screenLineBufferRange.start.row, targetBufferColumn])
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the end of the line.
|
||||
moveToEndOfScreenLine () {
|
||||
this.setScreenPosition([this.getScreenRow(), Infinity])
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the end of the buffer line.
|
||||
moveToEndOfLine () {
|
||||
this.setBufferPosition([this.getBufferRow(), Infinity])
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the beginning of the word.
|
||||
moveToBeginningOfWord () {
|
||||
this.setBufferPosition(this.getBeginningOfCurrentWordBufferPosition())
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the end of the word.
|
||||
moveToEndOfWord () {
|
||||
const position = this.getEndOfCurrentWordBufferPosition()
|
||||
if (position) this.setBufferPosition(position)
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the beginning of the next word.
|
||||
moveToBeginningOfNextWord () {
|
||||
const position = this.getBeginningOfNextWordBufferPosition()
|
||||
if (position) this.setBufferPosition(position)
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the previous word boundary.
|
||||
moveToPreviousWordBoundary () {
|
||||
const position = this.getPreviousWordBoundaryBufferPosition()
|
||||
if (position) this.setBufferPosition(position)
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the next word boundary.
|
||||
moveToNextWordBoundary () {
|
||||
const position = this.getNextWordBoundaryBufferPosition()
|
||||
if (position) this.setBufferPosition(position)
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the previous subword boundary.
|
||||
moveToPreviousSubwordBoundary () {
|
||||
const options = {wordRegex: this.subwordRegExp({backwards: true})}
|
||||
const position = this.getPreviousWordBoundaryBufferPosition(options)
|
||||
if (position) this.setBufferPosition(position)
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the next subword boundary.
|
||||
moveToNextSubwordBoundary () {
|
||||
const options = {wordRegex: this.subwordRegExp()}
|
||||
const position = this.getNextWordBoundaryBufferPosition(options)
|
||||
if (position) this.setBufferPosition(position)
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the beginning of the buffer line, skipping all
|
||||
// whitespace.
|
||||
skipLeadingWhitespace () {
|
||||
const position = this.getBufferPosition()
|
||||
const scanRange = this.getCurrentLineBufferRange()
|
||||
let endOfLeadingWhitespace = null
|
||||
this.editor.scanInBufferRange(/^[ \t]*/, scanRange, ({range}) => {
|
||||
endOfLeadingWhitespace = range.end
|
||||
})
|
||||
|
||||
if (endOfLeadingWhitespace.isGreaterThan(position)) this.setBufferPosition(endOfLeadingWhitespace)
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the beginning of the next paragraph
|
||||
moveToBeginningOfNextParagraph () {
|
||||
const position = this.getBeginningOfNextParagraphBufferPosition()
|
||||
if (position) this.setBufferPosition(position)
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the beginning of the previous paragraph
|
||||
moveToBeginningOfPreviousParagraph () {
|
||||
const position = this.getBeginningOfPreviousParagraphBufferPosition()
|
||||
if (position) this.setBufferPosition(position)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Local Positions and Ranges
|
||||
*/
|
||||
|
||||
// Public: Returns buffer position of previous word boundary. It might be on
|
||||
// the current word, or the previous word.
|
||||
//
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
// (default: {::wordRegExp})
|
||||
getPreviousWordBoundaryBufferPosition (options = {}) {
|
||||
const currentBufferPosition = this.getBufferPosition()
|
||||
const previousNonBlankRow = this.editor.buffer.previousNonBlankRow(currentBufferPosition.row)
|
||||
const scanRange = Range(Point(previousNonBlankRow || 0, 0), currentBufferPosition)
|
||||
|
||||
const ranges = this.editor.buffer.findAllInRangeSync(
|
||||
options.wordRegex || this.wordRegExp(),
|
||||
scanRange
|
||||
)
|
||||
|
||||
const range = ranges[ranges.length - 1]
|
||||
if (range) {
|
||||
if (range.start.row < currentBufferPosition.row && currentBufferPosition.column > 0) {
|
||||
return Point(currentBufferPosition.row, 0)
|
||||
} else if (currentBufferPosition.isGreaterThan(range.end)) {
|
||||
return Point.fromObject(range.end)
|
||||
} else {
|
||||
return Point.fromObject(range.start)
|
||||
}
|
||||
} else {
|
||||
return currentBufferPosition
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Returns buffer position of the next word boundary. It might be on
|
||||
// the current word, or the previous word.
|
||||
//
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
// (default: {::wordRegExp})
|
||||
getNextWordBoundaryBufferPosition (options = {}) {
|
||||
const currentBufferPosition = this.getBufferPosition()
|
||||
const scanRange = Range(currentBufferPosition, this.editor.getEofBufferPosition())
|
||||
|
||||
const range = this.editor.buffer.findInRangeSync(
|
||||
options.wordRegex || this.wordRegExp(),
|
||||
scanRange
|
||||
)
|
||||
|
||||
if (range) {
|
||||
if (range.start.row > currentBufferPosition.row) {
|
||||
return Point(range.start.row, 0)
|
||||
} else if (currentBufferPosition.isLessThan(range.start)) {
|
||||
return Point.fromObject(range.start)
|
||||
} else {
|
||||
return Point.fromObject(range.end)
|
||||
}
|
||||
} else {
|
||||
return currentBufferPosition
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Retrieves the buffer position of where the current word starts.
|
||||
//
|
||||
// * `options` (optional) An {Object} with the following keys:
|
||||
// * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
// (default: {::wordRegExp}).
|
||||
// * `includeNonWordCharacters` A {Boolean} indicating whether to include
|
||||
// non-word characters in the default word regex.
|
||||
// Has no effect if wordRegex is set.
|
||||
// * `allowPrevious` A {Boolean} indicating whether the beginning of the
|
||||
// previous word can be returned.
|
||||
//
|
||||
// Returns a {Range}.
|
||||
getBeginningOfCurrentWordBufferPosition (options = {}) {
|
||||
const allowPrevious = options.allowPrevious !== false
|
||||
const position = this.getBufferPosition()
|
||||
|
||||
const scanRange = allowPrevious
|
||||
? new Range(new Point(position.row - 1, 0), position)
|
||||
: new Range(new Point(position.row, 0), position)
|
||||
|
||||
const ranges = this.editor.buffer.findAllInRangeSync(
|
||||
options.wordRegex || this.wordRegExp(options),
|
||||
scanRange
|
||||
)
|
||||
|
||||
let result
|
||||
for (let range of ranges) {
|
||||
if (position.isLessThanOrEqual(range.start)) break
|
||||
if (allowPrevious || position.isLessThanOrEqual(range.end)) result = Point.fromObject(range.start)
|
||||
}
|
||||
|
||||
return result || (allowPrevious ? new Point(0, 0) : position)
|
||||
}
|
||||
|
||||
// Public: Retrieves the buffer position of where the current word ends.
|
||||
//
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
// (default: {::wordRegExp})
|
||||
// * `includeNonWordCharacters` A Boolean indicating whether to include
|
||||
// non-word characters in the default word regex. Has no effect if
|
||||
// wordRegex is set.
|
||||
//
|
||||
// Returns a {Range}.
|
||||
getEndOfCurrentWordBufferPosition (options = {}) {
|
||||
const allowNext = options.allowNext !== false
|
||||
const position = this.getBufferPosition()
|
||||
|
||||
const scanRange = allowNext
|
||||
? new Range(position, new Point(position.row + 2, 0))
|
||||
: new Range(position, new Point(position.row, Infinity))
|
||||
|
||||
const ranges = this.editor.buffer.findAllInRangeSync(
|
||||
options.wordRegex || this.wordRegExp(options),
|
||||
scanRange
|
||||
)
|
||||
|
||||
for (let range of ranges) {
|
||||
if (position.isLessThan(range.start) && !allowNext) break
|
||||
if (position.isLessThan(range.end)) return Point.fromObject(range.end)
|
||||
}
|
||||
|
||||
return allowNext ? this.editor.getEofBufferPosition() : position
|
||||
}
|
||||
|
||||
// Public: Retrieves the buffer position of where the next word starts.
|
||||
//
|
||||
// * `options` (optional) {Object}
|
||||
// * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
// (default: {::wordRegExp}).
|
||||
//
|
||||
// Returns a {Range}
|
||||
getBeginningOfNextWordBufferPosition (options = {}) {
|
||||
const currentBufferPosition = this.getBufferPosition()
|
||||
const start = this.isInsideWord(options) ? this.getEndOfCurrentWordBufferPosition(options) : currentBufferPosition
|
||||
const scanRange = [start, this.editor.getEofBufferPosition()]
|
||||
|
||||
let beginningOfNextWordPosition
|
||||
this.editor.scanInBufferRange(options.wordRegex || this.wordRegExp(), scanRange, ({range, stop}) => {
|
||||
beginningOfNextWordPosition = range.start
|
||||
stop()
|
||||
})
|
||||
|
||||
return beginningOfNextWordPosition || currentBufferPosition
|
||||
}
|
||||
|
||||
// Public: Returns the buffer Range occupied by the word located under the cursor.
|
||||
//
|
||||
// * `options` (optional) {Object}
|
||||
// * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
// (default: {::wordRegExp}).
|
||||
getCurrentWordBufferRange (options = {}) {
|
||||
const position = this.getBufferPosition()
|
||||
const ranges = this.editor.buffer.findAllInRangeSync(
|
||||
options.wordRegex || this.wordRegExp(options),
|
||||
new Range(new Point(position.row, 0), new Point(position.row, Infinity))
|
||||
)
|
||||
const range = ranges.find(range =>
|
||||
range.end.column >= position.column && range.start.column <= position.column
|
||||
)
|
||||
return range ? Range.fromObject(range) : new Range(position, position)
|
||||
}
|
||||
|
||||
// Public: Returns the buffer Range for the current line.
|
||||
//
|
||||
// * `options` (optional) {Object}
|
||||
// * `includeNewline` A {Boolean} which controls whether the Range should
|
||||
// include the newline.
|
||||
getCurrentLineBufferRange (options) {
|
||||
return this.editor.bufferRangeForBufferRow(this.getBufferRow(), options)
|
||||
}
|
||||
|
||||
// Public: Retrieves the range for the current paragraph.
|
||||
//
|
||||
// A paragraph is defined as a block of text surrounded by empty lines or comments.
|
||||
//
|
||||
// Returns a {Range}.
|
||||
getCurrentParagraphBufferRange () {
|
||||
return this.editor.rowRangeForParagraphAtBufferRow(this.getBufferRow())
|
||||
}
|
||||
|
||||
// Public: Returns the characters preceding the cursor in the current word.
|
||||
getCurrentWordPrefix () {
|
||||
return this.editor.getTextInBufferRange([this.getBeginningOfCurrentWordBufferPosition(), this.getBufferPosition()])
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Visibility
|
||||
*/
|
||||
|
||||
/*
|
||||
Section: Comparing to another cursor
|
||||
*/
|
||||
|
||||
// Public: Compare this cursor's buffer position to another cursor's buffer position.
|
||||
//
|
||||
// See {Point::compare} for more details.
|
||||
//
|
||||
// * `otherCursor`{Cursor} to compare against
|
||||
compare (otherCursor) {
|
||||
return this.getBufferPosition().compare(otherCursor.getBufferPosition())
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Utilities
|
||||
*/
|
||||
|
||||
// Public: Deselects the current selection.
|
||||
clearSelection (options) {
|
||||
if (this.selection) this.selection.clear(options)
|
||||
}
|
||||
|
||||
// Public: Get the RegExp used by the cursor to determine what a "word" is.
|
||||
//
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `includeNonWordCharacters` A {Boolean} indicating whether to include
|
||||
// non-word characters in the regex. (default: true)
|
||||
//
|
||||
// Returns a {RegExp}.
|
||||
wordRegExp (options) {
|
||||
const nonWordCharacters = _.escapeRegExp(this.getNonWordCharacters())
|
||||
let source = `^[\t ]*$|[^\\s${nonWordCharacters}]+`
|
||||
if (!options || options.includeNonWordCharacters !== false) {
|
||||
source += `|${`[${nonWordCharacters}]+`}`
|
||||
}
|
||||
return new RegExp(source, 'g')
|
||||
}
|
||||
|
||||
// Public: Get the RegExp used by the cursor to determine what a "subword" is.
|
||||
//
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `backwards` A {Boolean} indicating whether to look forwards or backwards
|
||||
// for the next subword. (default: false)
|
||||
//
|
||||
// Returns a {RegExp}.
|
||||
subwordRegExp (options = {}) {
|
||||
const nonWordCharacters = this.getNonWordCharacters()
|
||||
const lowercaseLetters = 'a-z\\u00DF-\\u00F6\\u00F8-\\u00FF'
|
||||
const uppercaseLetters = 'A-Z\\u00C0-\\u00D6\\u00D8-\\u00DE'
|
||||
const snakeCamelSegment = `[${uppercaseLetters}]?[${lowercaseLetters}]+`
|
||||
const segments = [
|
||||
'^[\t ]+',
|
||||
'[\t ]+$',
|
||||
`[${uppercaseLetters}]+(?![${lowercaseLetters}])`,
|
||||
'\\d+'
|
||||
]
|
||||
if (options.backwards) {
|
||||
segments.push(`${snakeCamelSegment}_*`)
|
||||
segments.push(`[${_.escapeRegExp(nonWordCharacters)}]+\\s*`)
|
||||
} else {
|
||||
segments.push(`_*${snakeCamelSegment}`)
|
||||
segments.push(`\\s*[${_.escapeRegExp(nonWordCharacters)}]+`)
|
||||
}
|
||||
segments.push('_+')
|
||||
return new RegExp(segments.join('|'), 'g')
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Private
|
||||
*/
|
||||
|
||||
getNonWordCharacters () {
|
||||
return this.editor.getNonWordCharacters(this.getBufferPosition())
|
||||
}
|
||||
|
||||
changePosition (options, fn) {
|
||||
this.clearSelection({autoscroll: false})
|
||||
fn()
|
||||
this.goalColumn = null
|
||||
const autoscroll = (options && options.autoscroll != null)
|
||||
? options.autoscroll
|
||||
: this.isLastCursor()
|
||||
if (autoscroll) this.autoscroll()
|
||||
}
|
||||
|
||||
getScreenRange () {
|
||||
const {row, column} = this.getScreenPosition()
|
||||
return new Range(new Point(row, column), new Point(row, column + 1))
|
||||
}
|
||||
|
||||
autoscroll (options = {}) {
|
||||
options.clip = false
|
||||
this.editor.scrollToScreenRange(this.getScreenRange(), options)
|
||||
}
|
||||
|
||||
getBeginningOfNextParagraphBufferPosition () {
|
||||
const start = this.getBufferPosition()
|
||||
const eof = this.editor.getEofBufferPosition()
|
||||
const scanRange = [start, eof]
|
||||
|
||||
const {row, column} = eof
|
||||
let position = new Point(row, column - 1)
|
||||
|
||||
this.editor.scanInBufferRange(EmptyLineRegExp, scanRange, ({range, stop}) => {
|
||||
position = range.start.traverse(Point(1, 0))
|
||||
if (!position.isEqual(start)) stop()
|
||||
})
|
||||
return position
|
||||
}
|
||||
|
||||
getBeginningOfPreviousParagraphBufferPosition () {
|
||||
const start = this.getBufferPosition()
|
||||
|
||||
const {row, column} = start
|
||||
const scanRange = [[row - 1, column], [0, 0]]
|
||||
let position = new Point(0, 0)
|
||||
this.editor.backwardsScanInBufferRange(EmptyLineRegExp, scanRange, ({range, stop}) => {
|
||||
position = range.start.traverse(Point(1, 0))
|
||||
if (!position.isEqual(start)) stop()
|
||||
})
|
||||
return position
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
module.exports =
|
||||
class CursorsComponent
|
||||
oldState: null
|
||||
|
||||
constructor: ->
|
||||
@cursorNodesById = {}
|
||||
@domNode = document.createElement('div')
|
||||
@domNode.classList.add('cursors')
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
updateSync: (state) ->
|
||||
newState = state.content
|
||||
@oldState ?= {cursors: {}}
|
||||
|
||||
# update blink class
|
||||
if newState.cursorsVisible isnt @oldState.cursorsVisible
|
||||
if newState.cursorsVisible
|
||||
@domNode.classList.remove 'blink-off'
|
||||
else
|
||||
@domNode.classList.add 'blink-off'
|
||||
@oldState.cursorsVisible = newState.cursorsVisible
|
||||
|
||||
# remove cursors
|
||||
for id of @oldState.cursors
|
||||
unless newState.cursors[id]?
|
||||
@cursorNodesById[id].remove()
|
||||
delete @cursorNodesById[id]
|
||||
delete @oldState.cursors[id]
|
||||
|
||||
# add or update cursors
|
||||
for id, cursorState of newState.cursors
|
||||
unless @oldState.cursors[id]?
|
||||
cursorNode = document.createElement('div')
|
||||
cursorNode.classList.add('cursor')
|
||||
@cursorNodesById[id] = cursorNode
|
||||
@domNode.appendChild(cursorNode)
|
||||
@updateCursorNode(id, cursorState)
|
||||
|
||||
return
|
||||
|
||||
updateCursorNode: (id, newCursorState) ->
|
||||
cursorNode = @cursorNodesById[id]
|
||||
oldCursorState = (@oldState.cursors[id] ?= {})
|
||||
|
||||
if newCursorState.top isnt oldCursorState.top or newCursorState.left isnt oldCursorState.left
|
||||
cursorNode.style['-webkit-transform'] = "translate(#{newCursorState.left}px, #{newCursorState.top}px)"
|
||||
oldCursorState.top = newCursorState.top
|
||||
oldCursorState.left = newCursorState.left
|
||||
|
||||
if newCursorState.height isnt oldCursorState.height
|
||||
cursorNode.style.height = newCursorState.height + 'px'
|
||||
oldCursorState.height = newCursorState.height
|
||||
|
||||
if newCursorState.width isnt oldCursorState.width
|
||||
cursorNode.style.width = newCursorState.width + 'px'
|
||||
oldCursorState.width = newCursorState.width
|
||||
@@ -1,109 +0,0 @@
|
||||
{setDimensionsAndBackground} = require './gutter-component-helpers'
|
||||
|
||||
# This class represents a gutter other than the 'line-numbers' gutter.
|
||||
# The contents of this gutter may be specified by Decorations.
|
||||
|
||||
module.exports =
|
||||
class CustomGutterComponent
|
||||
|
||||
constructor: ({@gutter, @views}) ->
|
||||
@decorationNodesById = {}
|
||||
@decorationItemsById = {}
|
||||
@visible = true
|
||||
|
||||
@domNode = @views.getView(@gutter)
|
||||
@decorationsNode = @domNode.firstChild
|
||||
# Clear the contents in case the domNode is being reused.
|
||||
@decorationsNode.innerHTML = ''
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
hideNode: ->
|
||||
if @visible
|
||||
@domNode.style.display = 'none'
|
||||
@visible = false
|
||||
|
||||
showNode: ->
|
||||
if not @visible
|
||||
@domNode.style.removeProperty('display')
|
||||
@visible = true
|
||||
|
||||
# `state` is a subset of the TextEditorPresenter state that is specific
|
||||
# to this line number gutter.
|
||||
updateSync: (state) ->
|
||||
@oldDimensionsAndBackgroundState ?= {}
|
||||
setDimensionsAndBackground(@oldDimensionsAndBackgroundState, state.styles, @decorationsNode)
|
||||
|
||||
@oldDecorationPositionState ?= {}
|
||||
decorationState = state.content
|
||||
|
||||
updatedDecorationIds = new Set
|
||||
for decorationId, decorationInfo of decorationState
|
||||
updatedDecorationIds.add(decorationId)
|
||||
existingDecoration = @decorationNodesById[decorationId]
|
||||
if existingDecoration
|
||||
@updateDecorationNode(existingDecoration, decorationId, decorationInfo)
|
||||
else
|
||||
newNode = @buildDecorationNode(decorationId, decorationInfo)
|
||||
@decorationNodesById[decorationId] = newNode
|
||||
@decorationsNode.appendChild(newNode)
|
||||
|
||||
for decorationId, decorationNode of @decorationNodesById
|
||||
if not updatedDecorationIds.has(decorationId)
|
||||
decorationNode.remove()
|
||||
delete @decorationNodesById[decorationId]
|
||||
delete @decorationItemsById[decorationId]
|
||||
delete @oldDecorationPositionState[decorationId]
|
||||
|
||||
###
|
||||
Section: Private Methods
|
||||
###
|
||||
|
||||
# Builds and returns an HTMLElement to represent the specified decoration.
|
||||
buildDecorationNode: (decorationId, decorationInfo) ->
|
||||
@oldDecorationPositionState[decorationId] = {}
|
||||
newNode = document.createElement('div')
|
||||
newNode.style.position = 'absolute'
|
||||
@updateDecorationNode(newNode, decorationId, decorationInfo)
|
||||
newNode
|
||||
|
||||
# Updates the existing HTMLNode with the new decoration info. Attempts to
|
||||
# minimize changes to the DOM.
|
||||
updateDecorationNode: (node, decorationId, newDecorationInfo) ->
|
||||
oldPositionState = @oldDecorationPositionState[decorationId]
|
||||
|
||||
if oldPositionState.top isnt newDecorationInfo.top + 'px'
|
||||
node.style.top = newDecorationInfo.top + 'px'
|
||||
oldPositionState.top = newDecorationInfo.top + 'px'
|
||||
|
||||
if oldPositionState.height isnt newDecorationInfo.height + 'px'
|
||||
node.style.height = newDecorationInfo.height + 'px'
|
||||
oldPositionState.height = newDecorationInfo.height + 'px'
|
||||
|
||||
if newDecorationInfo.class and not node.classList.contains(newDecorationInfo.class)
|
||||
node.className = 'decoration'
|
||||
node.classList.add(newDecorationInfo.class)
|
||||
else if not newDecorationInfo.class
|
||||
node.className = 'decoration'
|
||||
|
||||
@setDecorationItem(newDecorationInfo.item, newDecorationInfo.height, decorationId, node)
|
||||
|
||||
# Sets the decorationItem on the decorationNode.
|
||||
# If `decorationItem` is undefined, the decorationNode's child item will be cleared.
|
||||
setDecorationItem: (newItem, decorationHeight, decorationId, decorationNode) ->
|
||||
if newItem isnt @decorationItemsById[decorationId]
|
||||
while decorationNode.firstChild
|
||||
decorationNode.removeChild(decorationNode.firstChild)
|
||||
delete @decorationItemsById[decorationId]
|
||||
|
||||
if newItem
|
||||
newItemNode = null
|
||||
if newItem instanceof HTMLElement
|
||||
newItemNode = newItem
|
||||
else
|
||||
newItemNode = newItem.element
|
||||
|
||||
newItemNode.style.height = decorationHeight + 'px'
|
||||
decorationNode.appendChild(newItemNode)
|
||||
@decorationItemsById[decorationId] = newItem
|
||||
289
src/decoration-manager.js
Normal file
289
src/decoration-manager.js
Normal file
@@ -0,0 +1,289 @@
|
||||
const {Emitter} = require('event-kit')
|
||||
const Decoration = require('./decoration')
|
||||
const LayerDecoration = require('./layer-decoration')
|
||||
|
||||
module.exports =
|
||||
class DecorationManager {
|
||||
constructor (editor) {
|
||||
this.editor = editor
|
||||
this.displayLayer = this.editor.displayLayer
|
||||
|
||||
this.emitter = new Emitter()
|
||||
this.decorationCountsByLayer = new Map()
|
||||
this.markerDecorationCountsByLayer = new Map()
|
||||
this.decorationsByMarker = new Map()
|
||||
this.layerDecorationsByMarkerLayer = new Map()
|
||||
this.overlayDecorations = new Set()
|
||||
this.layerUpdateDisposablesByLayer = new WeakMap()
|
||||
}
|
||||
|
||||
observeDecorations (callback) {
|
||||
const decorations = this.getDecorations()
|
||||
for (let i = 0; i < decorations.length; i++) {
|
||||
callback(decorations[i])
|
||||
}
|
||||
return this.onDidAddDecoration(callback)
|
||||
}
|
||||
|
||||
onDidAddDecoration (callback) {
|
||||
return this.emitter.on('did-add-decoration', callback)
|
||||
}
|
||||
|
||||
onDidRemoveDecoration (callback) {
|
||||
return this.emitter.on('did-remove-decoration', callback)
|
||||
}
|
||||
|
||||
onDidUpdateDecorations (callback) {
|
||||
return this.emitter.on('did-update-decorations', callback)
|
||||
}
|
||||
|
||||
getDecorations (propertyFilter) {
|
||||
let allDecorations = []
|
||||
|
||||
this.decorationsByMarker.forEach((decorations) => {
|
||||
decorations.forEach((decoration) => allDecorations.push(decoration))
|
||||
})
|
||||
if (propertyFilter != null) {
|
||||
allDecorations = allDecorations.filter(function (decoration) {
|
||||
for (let key in propertyFilter) {
|
||||
const value = propertyFilter[key]
|
||||
if (decoration.properties[key] !== value) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
return allDecorations
|
||||
}
|
||||
|
||||
getLineDecorations (propertyFilter) {
|
||||
return this.getDecorations(propertyFilter).filter(decoration => decoration.isType('line'))
|
||||
}
|
||||
|
||||
getLineNumberDecorations (propertyFilter) {
|
||||
return this.getDecorations(propertyFilter).filter(decoration => decoration.isType('line-number'))
|
||||
}
|
||||
|
||||
getHighlightDecorations (propertyFilter) {
|
||||
return this.getDecorations(propertyFilter).filter(decoration => decoration.isType('highlight'))
|
||||
}
|
||||
|
||||
getOverlayDecorations (propertyFilter) {
|
||||
const result = []
|
||||
result.push(...Array.from(this.overlayDecorations))
|
||||
if (propertyFilter != null) {
|
||||
return result.filter(function (decoration) {
|
||||
for (let key in propertyFilter) {
|
||||
const value = propertyFilter[key]
|
||||
if (decoration.properties[key] !== value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
decorationPropertiesByMarkerForScreenRowRange (startScreenRow, endScreenRow) {
|
||||
const decorationPropertiesByMarker = new Map()
|
||||
|
||||
this.decorationCountsByLayer.forEach((count, markerLayer) => {
|
||||
const markers = markerLayer.findMarkers({intersectsScreenRowRange: [startScreenRow, endScreenRow - 1]})
|
||||
const layerDecorations = this.layerDecorationsByMarkerLayer.get(markerLayer)
|
||||
const hasMarkerDecorations = this.markerDecorationCountsByLayer.get(markerLayer) > 0
|
||||
|
||||
for (let i = 0; i < markers.length; i++) {
|
||||
const marker = markers[i]
|
||||
if (!marker.isValid()) continue
|
||||
|
||||
let decorationPropertiesForMarker = decorationPropertiesByMarker.get(marker)
|
||||
if (decorationPropertiesForMarker == null) {
|
||||
decorationPropertiesForMarker = []
|
||||
decorationPropertiesByMarker.set(marker, decorationPropertiesForMarker)
|
||||
}
|
||||
|
||||
if (layerDecorations) {
|
||||
layerDecorations.forEach((layerDecoration) => {
|
||||
const properties = layerDecoration.getPropertiesForMarker(marker) || layerDecoration.getProperties()
|
||||
decorationPropertiesForMarker.push(properties)
|
||||
})
|
||||
}
|
||||
|
||||
if (hasMarkerDecorations) {
|
||||
const decorationsForMarker = this.decorationsByMarker.get(marker)
|
||||
if (decorationsForMarker) {
|
||||
decorationsForMarker.forEach((decoration) => {
|
||||
decorationPropertiesForMarker.push(decoration.getProperties())
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return decorationPropertiesByMarker
|
||||
}
|
||||
|
||||
decorationsForScreenRowRange (startScreenRow, endScreenRow) {
|
||||
const decorationsByMarkerId = {}
|
||||
for (const layer of this.decorationCountsByLayer.keys()) {
|
||||
for (const marker of layer.findMarkers({intersectsScreenRowRange: [startScreenRow, endScreenRow]})) {
|
||||
const decorations = this.decorationsByMarker.get(marker)
|
||||
if (decorations) {
|
||||
decorationsByMarkerId[marker.id] = Array.from(decorations)
|
||||
}
|
||||
}
|
||||
}
|
||||
return decorationsByMarkerId
|
||||
}
|
||||
|
||||
decorationsStateForScreenRowRange (startScreenRow, endScreenRow) {
|
||||
const decorationsState = {}
|
||||
|
||||
for (const layer of this.decorationCountsByLayer.keys()) {
|
||||
for (const marker of layer.findMarkers({intersectsScreenRowRange: [startScreenRow, endScreenRow]})) {
|
||||
if (marker.isValid()) {
|
||||
const screenRange = marker.getScreenRange()
|
||||
const bufferRange = marker.getBufferRange()
|
||||
const rangeIsReversed = marker.isReversed()
|
||||
|
||||
const decorations = this.decorationsByMarker.get(marker)
|
||||
if (decorations) {
|
||||
decorations.forEach((decoration) => {
|
||||
decorationsState[decoration.id] = {
|
||||
properties: decoration.properties,
|
||||
screenRange,
|
||||
bufferRange,
|
||||
rangeIsReversed
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const layerDecorations = this.layerDecorationsByMarkerLayer.get(layer)
|
||||
if (layerDecorations) {
|
||||
layerDecorations.forEach((layerDecoration) => {
|
||||
const properties = layerDecoration.getPropertiesForMarker(marker) || layerDecoration.getProperties()
|
||||
decorationsState[`${layerDecoration.id}-${marker.id}`] = {
|
||||
properties,
|
||||
screenRange,
|
||||
bufferRange,
|
||||
rangeIsReversed
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return decorationsState
|
||||
}
|
||||
|
||||
decorateMarker (marker, decorationParams) {
|
||||
if (marker.isDestroyed()) {
|
||||
const error = new Error('Cannot decorate a destroyed marker')
|
||||
error.metadata = {markerLayerIsDestroyed: marker.layer.isDestroyed()}
|
||||
if (marker.destroyStackTrace != null) {
|
||||
error.metadata.destroyStackTrace = marker.destroyStackTrace
|
||||
}
|
||||
if (marker.bufferMarker != null && marker.bufferMarker.destroyStackTrace != null) {
|
||||
error.metadata.destroyStackTrace = marker.bufferMarker.destroyStackTrace
|
||||
}
|
||||
throw error
|
||||
}
|
||||
marker = this.displayLayer.getMarkerLayer(marker.layer.id).getMarker(marker.id)
|
||||
const decoration = new Decoration(marker, this, decorationParams)
|
||||
let decorationsForMarker = this.decorationsByMarker.get(marker)
|
||||
if (!decorationsForMarker) {
|
||||
decorationsForMarker = new Set()
|
||||
this.decorationsByMarker.set(marker, decorationsForMarker)
|
||||
}
|
||||
decorationsForMarker.add(decoration)
|
||||
if (decoration.isType('overlay')) this.overlayDecorations.add(decoration)
|
||||
this.observeDecoratedLayer(marker.layer, true)
|
||||
this.editor.didAddDecoration(decoration)
|
||||
this.emitDidUpdateDecorations()
|
||||
this.emitter.emit('did-add-decoration', decoration)
|
||||
return decoration
|
||||
}
|
||||
|
||||
decorateMarkerLayer (markerLayer, decorationParams) {
|
||||
if (markerLayer.isDestroyed()) {
|
||||
throw new Error('Cannot decorate a destroyed marker layer')
|
||||
}
|
||||
markerLayer = this.displayLayer.getMarkerLayer(markerLayer.id)
|
||||
const decoration = new LayerDecoration(markerLayer, this, decorationParams)
|
||||
let layerDecorations = this.layerDecorationsByMarkerLayer.get(markerLayer)
|
||||
if (layerDecorations == null) {
|
||||
layerDecorations = new Set()
|
||||
this.layerDecorationsByMarkerLayer.set(markerLayer, layerDecorations)
|
||||
}
|
||||
layerDecorations.add(decoration)
|
||||
this.observeDecoratedLayer(markerLayer, false)
|
||||
this.emitDidUpdateDecorations()
|
||||
return decoration
|
||||
}
|
||||
|
||||
emitDidUpdateDecorations () {
|
||||
this.editor.scheduleComponentUpdate()
|
||||
this.emitter.emit('did-update-decorations')
|
||||
}
|
||||
|
||||
decorationDidChangeType (decoration) {
|
||||
if (decoration.isType('overlay')) {
|
||||
this.overlayDecorations.add(decoration)
|
||||
} else {
|
||||
this.overlayDecorations.delete(decoration)
|
||||
}
|
||||
}
|
||||
|
||||
didDestroyMarkerDecoration (decoration) {
|
||||
const {marker} = decoration
|
||||
const decorations = this.decorationsByMarker.get(marker)
|
||||
if (decorations && decorations.has(decoration)) {
|
||||
decorations.delete(decoration)
|
||||
if (decorations.size === 0) this.decorationsByMarker.delete(marker)
|
||||
this.overlayDecorations.delete(decoration)
|
||||
this.unobserveDecoratedLayer(marker.layer, true)
|
||||
this.emitter.emit('did-remove-decoration', decoration)
|
||||
this.emitDidUpdateDecorations()
|
||||
}
|
||||
}
|
||||
|
||||
didDestroyLayerDecoration (decoration) {
|
||||
const {markerLayer} = decoration
|
||||
const decorations = this.layerDecorationsByMarkerLayer.get(markerLayer)
|
||||
|
||||
if (decorations && decorations.has(decoration)) {
|
||||
decorations.delete(decoration)
|
||||
if (decorations.size === 0) {
|
||||
this.layerDecorationsByMarkerLayer.delete(markerLayer)
|
||||
}
|
||||
this.unobserveDecoratedLayer(markerLayer, true)
|
||||
this.emitDidUpdateDecorations()
|
||||
}
|
||||
}
|
||||
|
||||
observeDecoratedLayer (layer, isMarkerDecoration) {
|
||||
const newCount = (this.decorationCountsByLayer.get(layer) || 0) + 1
|
||||
this.decorationCountsByLayer.set(layer, newCount)
|
||||
if (newCount === 1) {
|
||||
this.layerUpdateDisposablesByLayer.set(layer, layer.onDidUpdate(this.emitDidUpdateDecorations.bind(this)))
|
||||
}
|
||||
if (isMarkerDecoration) {
|
||||
this.markerDecorationCountsByLayer.set(layer, (this.markerDecorationCountsByLayer.get(layer) || 0) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
unobserveDecoratedLayer (layer, isMarkerDecoration) {
|
||||
const newCount = this.decorationCountsByLayer.get(layer) - 1
|
||||
if (newCount === 0) {
|
||||
this.layerUpdateDisposablesByLayer.get(layer).dispose()
|
||||
this.decorationCountsByLayer.delete(layer)
|
||||
} else {
|
||||
this.decorationCountsByLayer.set(layer, newCount)
|
||||
}
|
||||
if (isMarkerDecoration) {
|
||||
this.markerDecorationCountsByLayer.set(this.markerDecorationCountsByLayer.get(layer) - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
_ = require 'underscore-plus'
|
||||
{Emitter} = require 'event-kit'
|
||||
|
||||
idCounter = 0
|
||||
nextId = -> idCounter++
|
||||
|
||||
# Applies changes to a decorationsParam {Object} to make it possible to
|
||||
# differentiate decorations on custom gutters versus the line-number gutter.
|
||||
translateDecorationParamsOldToNew = (decorationParams) ->
|
||||
if decorationParams.type is 'line-number'
|
||||
decorationParams.gutterName = 'line-number'
|
||||
decorationParams
|
||||
|
||||
# Essential: Represents a decoration that follows a {TextEditorMarker}. A decoration is
|
||||
# basically a visual representation of a marker. It allows you to add CSS
|
||||
# classes to line numbers in the gutter, lines, and add selection-line regions
|
||||
# around marked ranges of text.
|
||||
#
|
||||
# {Decoration} objects are not meant to be created directly, but created with
|
||||
# {TextEditor::decorateMarker}. eg.
|
||||
#
|
||||
# ```coffee
|
||||
# range = editor.getSelectedBufferRange() # any range you like
|
||||
# marker = editor.markBufferRange(range)
|
||||
# decoration = editor.decorateMarker(marker, {type: 'line', class: 'my-line-class'})
|
||||
# ```
|
||||
#
|
||||
# Best practice for destroying the decoration is by destroying the {TextEditorMarker}.
|
||||
#
|
||||
# ```coffee
|
||||
# marker.destroy()
|
||||
# ```
|
||||
#
|
||||
# You should only use {Decoration::destroy} when you still need or do not own
|
||||
# the marker.
|
||||
module.exports =
|
||||
class Decoration
|
||||
# Private: Check if the `decorationProperties.type` matches `type`
|
||||
#
|
||||
# * `decorationProperties` {Object} eg. `{type: 'line-number', class: 'my-new-class'}`
|
||||
# * `type` {String} type like `'line-number'`, `'line'`, etc. `type` can also
|
||||
# be an {Array} of {String}s, where it will return true if the decoration's
|
||||
# type matches any in the array.
|
||||
#
|
||||
# Returns {Boolean}
|
||||
# Note: 'line-number' is a special subtype of the 'gutter' type. I.e., a
|
||||
# 'line-number' is a 'gutter', but a 'gutter' is not a 'line-number'.
|
||||
@isType: (decorationProperties, type) ->
|
||||
# 'line-number' is a special case of 'gutter'.
|
||||
if _.isArray(decorationProperties.type)
|
||||
return true if type in decorationProperties.type
|
||||
if type is 'gutter'
|
||||
return true if 'line-number' in decorationProperties.type
|
||||
return false
|
||||
else
|
||||
if type is 'gutter'
|
||||
return true if decorationProperties.type in ['gutter', 'line-number']
|
||||
else
|
||||
type is decorationProperties.type
|
||||
|
||||
###
|
||||
Section: Construction and Destruction
|
||||
###
|
||||
|
||||
constructor: (@marker, @displayBuffer, properties) ->
|
||||
@emitter = new Emitter
|
||||
@id = nextId()
|
||||
@setProperties properties
|
||||
@destroyed = false
|
||||
@markerDestroyDisposable = @marker.onDidDestroy => @destroy()
|
||||
|
||||
# Essential: Destroy this marker.
|
||||
#
|
||||
# If you own the marker, you should use {TextEditorMarker::destroy} which will destroy
|
||||
# this decoration.
|
||||
destroy: ->
|
||||
return if @destroyed
|
||||
@markerDestroyDisposable.dispose()
|
||||
@markerDestroyDisposable = null
|
||||
@destroyed = true
|
||||
@displayBuffer.didDestroyDecoration(this)
|
||||
@emitter.emit 'did-destroy'
|
||||
@emitter.dispose()
|
||||
|
||||
isDestroyed: -> @destroyed
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
###
|
||||
|
||||
# Essential: When the {Decoration} is updated via {Decoration::update}.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
# * `event` {Object}
|
||||
# * `oldProperties` {Object} the old parameters the decoration used to have
|
||||
# * `newProperties` {Object} the new parameters the decoration now has
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeProperties: (callback) ->
|
||||
@emitter.on 'did-change-properties', callback
|
||||
|
||||
# Essential: Invoke the given callback when the {Decoration} is destroyed
|
||||
#
|
||||
# * `callback` {Function}
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy: (callback) ->
|
||||
@emitter.on 'did-destroy', callback
|
||||
|
||||
###
|
||||
Section: Decoration Details
|
||||
###
|
||||
|
||||
# Essential: An id unique across all {Decoration} objects
|
||||
getId: -> @id
|
||||
|
||||
# Essential: Returns the marker associated with this {Decoration}
|
||||
getMarker: -> @marker
|
||||
|
||||
# Public: Check if this decoration is of type `type`
|
||||
#
|
||||
# * `type` {String} type like `'line-number'`, `'line'`, etc. `type` can also
|
||||
# be an {Array} of {String}s, where it will return true if the decoration's
|
||||
# type matches any in the array.
|
||||
#
|
||||
# Returns {Boolean}
|
||||
isType: (type) ->
|
||||
Decoration.isType(@properties, type)
|
||||
|
||||
###
|
||||
Section: Properties
|
||||
###
|
||||
|
||||
# Essential: Returns the {Decoration}'s properties.
|
||||
getProperties: ->
|
||||
@properties
|
||||
|
||||
# Essential: Update the marker with new Properties. Allows you to change the decoration's class.
|
||||
#
|
||||
# ## Examples
|
||||
#
|
||||
# ```coffee
|
||||
# decoration.update({type: 'line-number', class: 'my-new-class'})
|
||||
# ```
|
||||
#
|
||||
# * `newProperties` {Object} eg. `{type: 'line-number', class: 'my-new-class'}`
|
||||
setProperties: (newProperties) ->
|
||||
return if @destroyed
|
||||
oldProperties = @properties
|
||||
@properties = translateDecorationParamsOldToNew(newProperties)
|
||||
if newProperties.type?
|
||||
@displayBuffer.decorationDidChangeType(this)
|
||||
@displayBuffer.scheduleUpdateDecorationsEvent()
|
||||
@emitter.emit 'did-change-properties', {oldProperties, newProperties}
|
||||
|
||||
###
|
||||
Section: Utility
|
||||
###
|
||||
|
||||
inspect: ->
|
||||
"<Decoration #{@id}>"
|
||||
|
||||
###
|
||||
Section: Private methods
|
||||
###
|
||||
|
||||
matchesPattern: (decorationPattern) ->
|
||||
return false unless decorationPattern?
|
||||
for key, value of decorationPattern
|
||||
return false if @properties[key] isnt value
|
||||
true
|
||||
|
||||
flash: (klass, duration=500) ->
|
||||
@properties.flashCount ?= 0
|
||||
@properties.flashCount++
|
||||
@properties.flashClass = klass
|
||||
@properties.flashDuration = duration
|
||||
@displayBuffer.scheduleUpdateDecorationsEvent()
|
||||
@emitter.emit 'did-flash'
|
||||
204
src/decoration.js
Normal file
204
src/decoration.js
Normal file
@@ -0,0 +1,204 @@
|
||||
const {Emitter} = require('event-kit')
|
||||
|
||||
let idCounter = 0
|
||||
const nextId = () => idCounter++
|
||||
|
||||
// Applies changes to a decorationsParam {Object} to make it possible to
|
||||
// differentiate decorations on custom gutters versus the line-number gutter.
|
||||
const translateDecorationParamsOldToNew = function (decorationParams) {
|
||||
if (decorationParams.type === 'line-number') {
|
||||
decorationParams.gutterName = 'line-number'
|
||||
}
|
||||
return decorationParams
|
||||
}
|
||||
|
||||
// Essential: Represents a decoration that follows a {DisplayMarker}. A decoration is
|
||||
// basically a visual representation of a marker. It allows you to add CSS
|
||||
// classes to line numbers in the gutter, lines, and add selection-line regions
|
||||
// around marked ranges of text.
|
||||
//
|
||||
// {Decoration} objects are not meant to be created directly, but created with
|
||||
// {TextEditor::decorateMarker}. eg.
|
||||
//
|
||||
// ```coffee
|
||||
// range = editor.getSelectedBufferRange() # any range you like
|
||||
// marker = editor.markBufferRange(range)
|
||||
// decoration = editor.decorateMarker(marker, {type: 'line', class: 'my-line-class'})
|
||||
// ```
|
||||
//
|
||||
// Best practice for destroying the decoration is by destroying the {DisplayMarker}.
|
||||
//
|
||||
// ```coffee
|
||||
// marker.destroy()
|
||||
// ```
|
||||
//
|
||||
// You should only use {Decoration::destroy} when you still need or do not own
|
||||
// the marker.
|
||||
module.exports =
|
||||
class Decoration {
|
||||
// Private: Check if the `decorationProperties.type` matches `type`
|
||||
//
|
||||
// * `decorationProperties` {Object} eg. `{type: 'line-number', class: 'my-new-class'}`
|
||||
// * `type` {String} type like `'line-number'`, `'line'`, etc. `type` can also
|
||||
// be an {Array} of {String}s, where it will return true if the decoration's
|
||||
// type matches any in the array.
|
||||
//
|
||||
// Returns {Boolean}
|
||||
// Note: 'line-number' is a special subtype of the 'gutter' type. I.e., a
|
||||
// 'line-number' is a 'gutter', but a 'gutter' is not a 'line-number'.
|
||||
static isType (decorationProperties, type) {
|
||||
// 'line-number' is a special case of 'gutter'.
|
||||
if (Array.isArray(decorationProperties.type)) {
|
||||
if (decorationProperties.type.includes(type)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (type === 'gutter' && decorationProperties.type.includes('line-number')) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
} else {
|
||||
if (type === 'gutter') {
|
||||
return ['gutter', 'line-number'].includes(decorationProperties.type)
|
||||
} else {
|
||||
return type === decorationProperties.type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Construction and Destruction
|
||||
*/
|
||||
|
||||
constructor (marker, decorationManager, properties) {
|
||||
this.marker = marker
|
||||
this.decorationManager = decorationManager
|
||||
this.emitter = new Emitter()
|
||||
this.id = nextId()
|
||||
this.setProperties(properties)
|
||||
this.destroyed = false
|
||||
this.markerDestroyDisposable = this.marker.onDidDestroy(() => this.destroy())
|
||||
}
|
||||
|
||||
// Essential: Destroy this marker decoration.
|
||||
//
|
||||
// You can also destroy the marker if you own it, which will destroy this
|
||||
// decoration.
|
||||
destroy () {
|
||||
if (this.destroyed) { return }
|
||||
this.markerDestroyDisposable.dispose()
|
||||
this.markerDestroyDisposable = null
|
||||
this.destroyed = true
|
||||
this.decorationManager.didDestroyMarkerDecoration(this)
|
||||
this.emitter.emit('did-destroy')
|
||||
return this.emitter.dispose()
|
||||
}
|
||||
|
||||
isDestroyed () { return this.destroyed }
|
||||
|
||||
/*
|
||||
Section: Event Subscription
|
||||
*/
|
||||
|
||||
// Essential: When the {Decoration} is updated via {Decoration::update}.
|
||||
//
|
||||
// * `callback` {Function}
|
||||
// * `event` {Object}
|
||||
// * `oldProperties` {Object} the old parameters the decoration used to have
|
||||
// * `newProperties` {Object} the new parameters the decoration now has
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeProperties (callback) {
|
||||
return this.emitter.on('did-change-properties', callback)
|
||||
}
|
||||
|
||||
// Essential: Invoke the given callback when the {Decoration} is destroyed
|
||||
//
|
||||
// * `callback` {Function}
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy (callback) {
|
||||
return this.emitter.once('did-destroy', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Decoration Details
|
||||
*/
|
||||
|
||||
// Essential: An id unique across all {Decoration} objects
|
||||
getId () { return this.id }
|
||||
|
||||
// Essential: Returns the marker associated with this {Decoration}
|
||||
getMarker () { return this.marker }
|
||||
|
||||
// Public: Check if this decoration is of type `type`
|
||||
//
|
||||
// * `type` {String} type like `'line-number'`, `'line'`, etc. `type` can also
|
||||
// be an {Array} of {String}s, where it will return true if the decoration's
|
||||
// type matches any in the array.
|
||||
//
|
||||
// Returns {Boolean}
|
||||
isType (type) {
|
||||
return Decoration.isType(this.properties, type)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Properties
|
||||
*/
|
||||
|
||||
// Essential: Returns the {Decoration}'s properties.
|
||||
getProperties () {
|
||||
return this.properties
|
||||
}
|
||||
|
||||
// Essential: Update the marker with new Properties. Allows you to change the decoration's class.
|
||||
//
|
||||
// ## Examples
|
||||
//
|
||||
// ```coffee
|
||||
// decoration.setProperties({type: 'line-number', class: 'my-new-class'})
|
||||
// ```
|
||||
//
|
||||
// * `newProperties` {Object} eg. `{type: 'line-number', class: 'my-new-class'}`
|
||||
setProperties (newProperties) {
|
||||
if (this.destroyed) { return }
|
||||
const oldProperties = this.properties
|
||||
this.properties = translateDecorationParamsOldToNew(newProperties)
|
||||
if (newProperties.type != null) {
|
||||
this.decorationManager.decorationDidChangeType(this)
|
||||
}
|
||||
this.decorationManager.emitDidUpdateDecorations()
|
||||
return this.emitter.emit('did-change-properties', {oldProperties, newProperties})
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Utility
|
||||
*/
|
||||
|
||||
inspect () {
|
||||
return `<Decoration ${this.id}>`
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Private methods
|
||||
*/
|
||||
|
||||
matchesPattern (decorationPattern) {
|
||||
if (decorationPattern == null) { return false }
|
||||
for (let key in decorationPattern) {
|
||||
const value = decorationPattern[key]
|
||||
if (this.properties[key] !== value) { return false }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
flash (klass, duration) {
|
||||
if (duration == null) { duration = 500 }
|
||||
this.properties.flashRequested = true
|
||||
this.properties.flashClass = klass
|
||||
this.properties.flashDuration = duration
|
||||
this.decorationManager.emitDidUpdateDecorations()
|
||||
return this.emitter.emit('did-flash')
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,11 @@ class DefaultDirectoryProvider
|
||||
#
|
||||
# Returns:
|
||||
# * {Directory} if the given URI is compatible with this provider.
|
||||
# * `null` if the given URI is not compatibile with this provider.
|
||||
# * `null` if the given URI is not compatible with this provider.
|
||||
directoryForURISync: (uri) ->
|
||||
normalizedPath = path.normalize(uri)
|
||||
{protocol} = url.parse(uri)
|
||||
directoryPath = if protocol?
|
||||
normalizedPath = @normalizePath(uri)
|
||||
{host} = url.parse(uri)
|
||||
directoryPath = if host
|
||||
uri
|
||||
else if not fs.isDirectorySync(normalizedPath) and fs.isDirectorySync(path.dirname(normalizedPath))
|
||||
path.dirname(normalizedPath)
|
||||
@@ -26,7 +26,7 @@ class DefaultDirectoryProvider
|
||||
|
||||
# TODO: Stop normalizing the path in pathwatcher's Directory.
|
||||
directory = new Directory(directoryPath)
|
||||
if protocol?
|
||||
if host
|
||||
directory.path = directoryPath
|
||||
if fs.isCaseInsensitive()
|
||||
directory.lowerCasePath = directoryPath.toLowerCase()
|
||||
@@ -39,6 +39,20 @@ class DefaultDirectoryProvider
|
||||
#
|
||||
# Returns a {Promise} that resolves to:
|
||||
# * {Directory} if the given URI is compatible with this provider.
|
||||
# * `null` if the given URI is not compatibile with this provider.
|
||||
# * `null` if the given URI is not compatible with this provider.
|
||||
directoryForURI: (uri) ->
|
||||
Promise.resolve(@directoryForURISync(uri))
|
||||
|
||||
# Public: Normalizes path.
|
||||
#
|
||||
# * `uri` {String} The path that should be normalized.
|
||||
#
|
||||
# Returns a {String} with normalized path.
|
||||
normalizePath: (uri) ->
|
||||
# Normalize disk drive letter on Windows to avoid opening two buffers for the same file
|
||||
pathWithNormalizedDiskDriveLetter =
|
||||
if process.platform is 'win32' and matchData = uri.match(/^([a-z]):/)
|
||||
"#{matchData[1].toUpperCase()}#{uri.slice(1)}"
|
||||
else
|
||||
uri
|
||||
path.normalize(pathWithNormalizedDiskDriveLetter)
|
||||
|
||||
@@ -11,13 +11,16 @@ class DirectorySearch
|
||||
excludeVcsIgnores: options.excludeVcsIgnores
|
||||
globalExclusions: options.exclusions
|
||||
follow: options.follow
|
||||
searchOptions =
|
||||
leadingContextLineCount: options.leadingContextLineCount
|
||||
trailingContextLineCount: options.trailingContextLineCount
|
||||
@task = new Task(require.resolve('./scan-handler'))
|
||||
@task.on 'scan:result-found', options.didMatch
|
||||
@task.on 'scan:file-error', options.didError
|
||||
@task.on 'scan:paths-searched', options.didSearchPaths
|
||||
@promise = new Promise (resolve, reject) =>
|
||||
@task.on('task:cancelled', reject)
|
||||
@task.start rootPaths, regex.source, scanHandlerOptions, =>
|
||||
@task.start rootPaths, regex.source, scanHandlerOptions, searchOptions, =>
|
||||
@task.terminate()
|
||||
resolve()
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
semver = require 'semver'
|
||||
|
||||
deprecatedPackages = require('../package.json')?._deprecatedPackages ? {}
|
||||
ranges = {}
|
||||
|
||||
exports.getDeprecatedPackageMetadata = (name) ->
|
||||
metadata = null
|
||||
if deprecatedPackages.hasOwnProperty(name)
|
||||
metadata = deprecatedPackages[name]
|
||||
Object.freeze(metadata) if metadata
|
||||
metadata
|
||||
|
||||
exports.isDeprecatedPackage = (name, version) ->
|
||||
return false unless deprecatedPackages.hasOwnProperty(name)
|
||||
|
||||
deprecatedVersionRange = deprecatedPackages[name].version
|
||||
return true unless deprecatedVersionRange
|
||||
|
||||
semver.valid(version) and satisfies(version, deprecatedVersionRange)
|
||||
|
||||
satisfies = (version, rawRange) ->
|
||||
unless parsedRange = ranges[rawRange]
|
||||
parsedRange = new Range(rawRange)
|
||||
ranges[rawRange] = parsedRange
|
||||
parsedRange.test(version)
|
||||
|
||||
# Extend semver.Range to memoize matched versions for speed
|
||||
class Range extends semver.Range
|
||||
constructor: ->
|
||||
super
|
||||
@matchedVersions = new Set()
|
||||
@unmatchedVersions = new Set()
|
||||
|
||||
test: (version) ->
|
||||
return true if @matchedVersions.has(version)
|
||||
return false if @unmatchedVersions.has(version)
|
||||
|
||||
matches = super
|
||||
if matches
|
||||
@matchedVersions.add(version)
|
||||
else
|
||||
@unmatchedVersions.add(version)
|
||||
matches
|
||||
964
src/deprecated-syntax-selectors.js
Normal file
964
src/deprecated-syntax-selectors.js
Normal file
@@ -0,0 +1,964 @@
|
||||
module.exports = new Set([
|
||||
'AFDKO', 'AFKDO', 'ASS', 'AVX', 'AVX2', 'AVX512', 'AVX512BW', 'AVX512DQ',
|
||||
'Alignment', 'Alpha', 'AlphaLevel', 'Angle', 'Animation', 'AnimationGroup',
|
||||
'ArchaeologyDigSiteFrame', 'Arrow__', 'AtLilyPond', 'AttrBaseType',
|
||||
'AttrSetVal__', 'BackColour', 'Banner', 'Bold', 'Bonlang', 'BorderStyle',
|
||||
'Browser', 'Button', 'C99', 'CALCULATE', 'CharacterSet', 'ChatScript',
|
||||
'Chatscript', 'CheckButton', 'ClipboardFormat', 'ClipboardType',
|
||||
'Clipboard__', 'CodePage', 'Codepages__', 'Collisions', 'ColorSelect',
|
||||
'ColourActual', 'ColourLogical', 'ColourReal', 'ColourScheme', 'ColourSize',
|
||||
'Column', 'Comment', 'ConfCachePolicy', 'ControlPoint', 'Cooldown', 'DBE',
|
||||
'DDL', 'DML', 'DSC', 'Database__', 'DdcMode', 'Dialogue',
|
||||
'DiscussionFilterType', 'DiscussionStatus', 'DisplaySchemes',
|
||||
'Document-Structuring-Comment', 'DressUpModel', 'Edit', 'EditBox', 'Effect',
|
||||
'Encoding', 'End', 'ExternalLinkBehaviour', 'ExternalLinkDirection', 'F16c',
|
||||
'FMA', 'FilterType', 'Font', 'FontInstance', 'FontString', 'Fontname',
|
||||
'Fonts__', 'Fontsize', 'Format', 'Frame', 'GameTooltip', 'GroupList', 'HLE',
|
||||
'HeaderEvent', 'HistoryType', 'HttpVerb', 'II', 'IO', 'Icon', 'IconID',
|
||||
'InPlaceBox__', 'InPlaceEditEvent', 'Info', 'Italic', 'JSXEndTagStart',
|
||||
'JSXStartTagEnd', 'KNC', 'KeyModifier', 'Kotlin', 'LUW', 'Language', 'Layer',
|
||||
'LayeredRegion', 'LdapItemList', 'LineSpacing', 'LinkFilter', 'LinkLimit',
|
||||
'ListView', 'Locales__', 'Lock', 'LoginPolicy', 'MA_End__', 'MA_StdCombo__',
|
||||
'MA_StdItem__', 'MA_StdMenu__', 'MISSING', 'Mapping', 'MarginL', 'MarginR',
|
||||
'MarginV', 'Marked', 'MessageFrame', 'Minimap', 'MovieFrame', 'Name',
|
||||
'Outline', 'OutlineColour', 'ParentedObject', 'Path', 'Permission', 'PlayRes',
|
||||
'PlayerModel', 'PrimaryColour', 'Proof', 'QuestPOIFrame', 'RTM',
|
||||
'RecentModule__', 'Regexp', 'Region', 'Rotation', 'SCADABasic', 'SSA',
|
||||
'Scale', 'ScaleX', 'ScaleY', 'ScaledBorderAndShadow', 'ScenarioPOIFrame',
|
||||
'ScriptObject', 'Script__', 'Scroll', 'ScrollEvent', 'ScrollFrame',
|
||||
'ScrollSide', 'ScrollingMessageFrame', 'SecondaryColour', 'Sensitivity',
|
||||
'Shadow', 'SimpleHTML', 'Slider', 'Spacing', 'Start', 'StatusBar', 'Stream',
|
||||
'StrikeOut', 'Style', 'TIS', 'TODO', 'TabardModel', 'Text', 'Texture',
|
||||
'Timer', 'ToolType', 'Translation', 'TreeView', 'TriggerStatus', 'UIObject',
|
||||
'Underline', 'UserClass', 'UserList', 'UserNotifyList', 'VisibleRegion',
|
||||
'Vplus', 'WrapStyle', 'XHPEndTagStart', 'XHPStartTagEnd', 'ZipType',
|
||||
'__package-name__', '_c', '_function', 'a', 'a10networks', 'aaa', 'abaqus',
|
||||
'abbrev', 'abbreviated', 'abbreviation', 'abcnotation', 'abl', 'abnf', 'abp',
|
||||
'absolute', 'abstract', 'academic', 'access', 'access-control',
|
||||
'access-qualifiers', 'accessed', 'accessor', 'account', 'accumulator', 'ace',
|
||||
'ace3', 'acl', 'acos', 'act', 'action', 'action-map', 'actionhandler',
|
||||
'actionpack', 'actions', 'actionscript', 'activerecord', 'activesupport',
|
||||
'actual', 'acute-accent', 'ada', 'add', 'adddon', 'added', 'addition',
|
||||
'additional-character', 'additive', 'addon', 'address', 'address-of',
|
||||
'address-space', 'addrfam', 'adjustment', 'admonition', 'adr', 'adverb',
|
||||
'adx', 'ael', 'aem', 'aerospace', 'aes', 'aes_functions', 'aesni',
|
||||
'aexLightGreen', 'af', 'afii', 'aflex', 'after', 'after-expression', 'agc',
|
||||
'agda', 'agentspeak', 'aggregate', 'aggregation', 'ahk', 'ai-connection',
|
||||
'ai-player', 'ai-wheeled-vehicle', 'aif', 'alabel', 'alarms', 'alda', 'alert',
|
||||
'algebraic-type', 'alias', 'aliases', 'align', 'align-attribute', 'alignment',
|
||||
'alignment-cue-setting', 'alignment-mode', 'all', 'all-once', 'all-solutions',
|
||||
'allocate', 'alloy', 'alloyglobals', 'alloyxml', 'alog', 'alpha',
|
||||
'alphabeticalllt', 'alphabeticallyge', 'alphabeticallygt', 'alphabeticallyle',
|
||||
'alt', 'alter', 'alternate-wysiwyg-string', 'alternates', 'alternation',
|
||||
'alternatives', 'am', 'ambient-audio-manager', 'ambient-reflectivity', 'amd',
|
||||
'amd3DNow', 'amdnops', 'ameter', 'amount', 'amp', 'ampersand', 'ampl',
|
||||
'ampscript', 'an', 'analysis', 'analytics', 'anb', 'anchor', 'and', 'andop',
|
||||
'angelscript', 'angle', 'angle-brackets', 'angular', 'animation', 'annot',
|
||||
'annotated', 'annotation', 'annotation-arguments', 'anon', 'anonymous',
|
||||
'another', 'ansi', 'ansi-c', 'ansi-colored', 'ansi-escape-code',
|
||||
'ansi-formatted', 'ansi2', 'ansible', 'answer', 'antialiasing', 'antl',
|
||||
'antlr', 'antlr4', 'anubis', 'any', 'any-method', 'anyclass', 'aolserver',
|
||||
'apa', 'apache', 'apache-config', 'apc', 'apdl', 'apex', 'api',
|
||||
'api-notation', 'apiary', 'apib', 'apl', 'apostrophe', 'appcache',
|
||||
'applescript', 'application', 'application-name', 'application-process',
|
||||
'approx-equal', 'aql', 'aqua', 'ar', 'arbitrary-radix',
|
||||
'arbitrary-repetition', 'arbitrary-repitition', 'arch', 'arch_specification',
|
||||
'architecture', 'archive', 'archives', 'arduino', 'area-code', 'arendelle',
|
||||
'argcount', 'args', 'argument', 'argument-label', 'argument-separator',
|
||||
'argument-seperator', 'argument-type', 'arguments', 'arith', 'arithmetic',
|
||||
'arithmetical', 'arithmeticcql', 'ark', 'arm', 'arma', 'armaConfig',
|
||||
'arnoldc', 'arp', 'arpop', 'arr', 'array', 'array-expression',
|
||||
'array-literal', 'arrays', 'arrow', 'articulation', 'artihmetic', 'arvo',
|
||||
'aryop', 'as', 'as4', 'ascii', 'asciidoc', 'asdoc', 'ash', 'ashx', 'asl',
|
||||
'asm', 'asm-instruction', 'asm-type-prefix', 'asn', 'asp', 'asp-core-2',
|
||||
'aspx', 'ass', 'assembly', 'assert', 'assertion', 'assigment', 'assign',
|
||||
'assign-class', 'assigned', 'assigned-class', 'assigned-value', 'assignee',
|
||||
'assignement', 'assignment', 'assignmentforge-config', 'associate',
|
||||
'association', 'associativity', 'assocs', 'asterisk', 'async', 'at-marker',
|
||||
'at-root', 'at-rule', 'at-sign', 'atmark', 'atml3', 'atoemp', 'atom',
|
||||
'atom-term-processing', 'atomic', 'atomscript', 'att', 'attachment', 'attr',
|
||||
'attribute', 'attribute-entry', 'attribute-expression', 'attribute-key-value',
|
||||
'attribute-list', 'attribute-lookup', 'attribute-name', 'attribute-reference',
|
||||
'attribute-selector', 'attribute-value', 'attribute-values',
|
||||
'attribute-with-value', 'attribute_list', 'attribute_value',
|
||||
'attribute_value2', 'attributelist', 'attributes', 'attrset',
|
||||
'attrset-or-function', 'audio', 'audio-file', 'auditor', 'augmented', 'auth',
|
||||
'auth_basic', 'author', 'author-names', 'authorization', 'auto', 'auto-event',
|
||||
'autoconf', 'autoindex', 'autoit', 'automake', 'automatic', 'autotools',
|
||||
'autovar', 'aux', 'auxiliary', 'avdl', 'avra', 'avrasm', 'avrdisasm', 'avs',
|
||||
'avx', 'avx2', 'avx512', 'awk', 'axes_group', 'axis', 'axl', 'b',
|
||||
'b-spline-patch', 'babel', 'back', 'back-from', 'back-reference',
|
||||
'back-slash', 'backend', 'background', 'backreference', 'backslash',
|
||||
'backslash-bar', 'backslash-g', 'backspace', 'backtick', 'bad-ampersand',
|
||||
'bad-angle-bracket', 'bad-assignment', 'bad-comments-or-CDATA', 'bad-escape',
|
||||
'bad-octal', 'bad-var', 'bang', 'banner', 'bar', 'bareword', 'barline',
|
||||
'base', 'base-11', 'base-12', 'base-13', 'base-14', 'base-15', 'base-16',
|
||||
'base-17', 'base-18', 'base-19', 'base-20', 'base-21', 'base-22', 'base-23',
|
||||
'base-24', 'base-25', 'base-26', 'base-27', 'base-28', 'base-29', 'base-3',
|
||||
'base-30', 'base-31', 'base-32', 'base-33', 'base-34', 'base-35', 'base-36',
|
||||
'base-4', 'base-5', 'base-6', 'base-7', 'base-9', 'base-call', 'base-integer',
|
||||
'base64', 'base85', 'base_pound_number_pound', 'basetype', 'basic',
|
||||
'basic-arithmetic', 'basic-type', 'basic_functions', 'basicblock',
|
||||
'basis-matrix', 'bat', 'batch', 'batchfile', 'battlesim', 'bb', 'bbcode',
|
||||
'bcmath', 'be', 'beam', 'beamer', 'beancount', 'before', 'begin',
|
||||
'begin-document', 'begin-emphasis', 'begin-end', 'begin-end-group',
|
||||
'begin-literal', 'begin-symbolic', 'begintimeblock', 'behaviour', 'bem',
|
||||
'between-tag-pair', 'bevel', 'bezier-patch', 'bfeac', 'bff', 'bg', 'bg-black',
|
||||
'bg-blue', 'bg-cyan', 'bg-green', 'bg-normal', 'bg-purple', 'bg-red',
|
||||
'bg-white', 'bg-yellow', 'bhtml', 'bhv', 'bibitem', 'bibliography-anchor',
|
||||
'biblioref', 'bibpaper', 'bibtex', 'bif', 'big-arrow', 'big-arrow-left',
|
||||
'bigdecimal', 'bigint', 'biicode', 'biiconf', 'bin', 'binOp', 'binary',
|
||||
'binary-arithmetic', 'bind', 'binder', 'binding', 'binding-prefix',
|
||||
'bindings', 'binop', 'bioinformatics', 'biosphere', 'bird-track', 'bis',
|
||||
'bison', 'bit', 'bit-and-byte', 'bit-range', 'bit-wise', 'bitarray', 'bitop',
|
||||
'bits-mov', 'bitvector', 'bitwise', 'black', 'blade', 'blanks', 'blaze',
|
||||
'blenc', 'blend', 'blending', 'blendtype', 'blendu', 'blendv', 'blip',
|
||||
'block', 'block-attribute', 'block-dartdoc', 'block-data', 'block-level',
|
||||
'blockid', 'blockname', 'blockquote', 'blocktitle', 'blue', 'blueprint',
|
||||
'bluespec', 'blur', 'bm', 'bmi', 'bmi1', 'bmi2', 'bnd', 'bnf', 'body',
|
||||
'body-statement', 'bold', 'bold-italic-text', 'bold-text', 'bolt', 'bond',
|
||||
'bonlang', 'boo', 'boogie', 'bool', 'boolean', 'boolean-test', 'boost',
|
||||
'boot', 'bord', 'border', 'botml', 'bottom', 'boundary', 'bounded', 'bounds',
|
||||
'bow', 'box', 'bpl', 'bpr', 'bqparam', 'brace', 'braced', 'braces', 'bracket',
|
||||
'bracketed', 'brackets', 'brainfuck', 'branch', 'branch-point', 'break',
|
||||
'breakpoint', 'breakpoints', 'breaks', 'bridle', 'brightscript', 'bro',
|
||||
'broken', 'browser', 'browsers', 'bs', 'bsl', 'btw', 'buffered', 'buffers',
|
||||
'bugzilla-number', 'build', 'buildin', 'buildout', 'built-in',
|
||||
'built-in-variable', 'built-ins', 'builtin', 'builtin-comparison', 'builtins',
|
||||
'bullet', 'bullet-point', 'bump', 'bump-multiplier', 'bundle', 'but',
|
||||
'button', 'buttons', 'by', 'by-name', 'by-number', 'byref', 'byte',
|
||||
'bytearray', 'bz2', 'bzl', 'c', 'c-style', 'c0', 'c1', 'c2hs', 'ca', 'cabal',
|
||||
'cabal-keyword', 'cache', 'cache-management', 'cacheability-control', 'cake',
|
||||
'calc', 'calca', 'calendar', 'call', 'callable', 'callback', 'caller',
|
||||
'calling', 'callmethod', 'callout', 'callparent', 'camera', 'camlp4',
|
||||
'camlp4-stream', 'canonicalized-program-name', 'canopen', 'capability',
|
||||
'capnp', 'cappuccino', 'caps', 'caption', 'capture', 'capturename',
|
||||
'cardinal-curve', 'cardinal-patch', 'cascade', 'case', 'case-block',
|
||||
'case-body', 'case-class', 'case-clause', 'case-clause-body',
|
||||
'case-expression', 'case-modifier', 'case-pattern', 'case-statement',
|
||||
'case-terminator', 'case-value', 'cassius', 'cast', 'catch',
|
||||
'catch-exception', 'catcode', 'categories', 'categort', 'category', 'cba',
|
||||
'cbmbasic', 'cbot', 'cbs', 'cc', 'cc65', 'ccml', 'cdata', 'cdef', 'cdtor',
|
||||
'ceiling', 'cell', 'cellcontents', 'cellwall', 'ceq', 'ces', 'cet', 'cexpr',
|
||||
'cextern', 'ceylon', 'ceylondoc', 'cf', 'cfdg', 'cfengine', 'cfg', 'cfml',
|
||||
'cfscript', 'cfunction', 'cg', 'cgi', 'cgx', 'chain', 'chained', 'chaining',
|
||||
'chainname', 'changed', 'changelogs', 'changes', 'channel', 'chapel',
|
||||
'chapter', 'char', 'characater', 'character', 'character-class',
|
||||
'character-data-not-allowed-here', 'character-literal',
|
||||
'character-literal-too-long', 'character-not-allowed-here', 'character-range',
|
||||
'character-reference', 'character-token', 'character_not_allowed',
|
||||
'character_not_allowed_here', 'characters', 'chars', 'chars-and-bytes-io',
|
||||
'charset', 'check', 'check-identifier', 'checkboxes', 'checker', 'chef',
|
||||
'chem', 'chemical', 'children', 'choice', 'choicescript', 'chord', 'chorus',
|
||||
'chuck', 'chunk', 'ciexyz', 'circle', 'circle-jot', 'cirru', 'cisco',
|
||||
'cisco-ios-config', 'citation', 'cite', 'citrine', 'cjam', 'cjson', 'clamp',
|
||||
'clamping', 'class', 'class-constraint', 'class-constraints',
|
||||
'class-declaration', 'class-definition', 'class-fns', 'class-instance',
|
||||
'class-list', 'class-struct-block', 'class-type', 'class-type-definition',
|
||||
'classcode', 'classes', 'classic', 'classicalb', 'classmethods', 'classobj',
|
||||
'classtree', 'clause', 'clause-head-body', 'clauses', 'clear',
|
||||
'clear-argument', 'cleared', 'clflushopt', 'click', 'client', 'client-server',
|
||||
'clip', 'clipboard', 'clips', 'clmul', 'clock', 'clojure', 'cloned', 'close',
|
||||
'closed', 'closing', 'closing-text', 'closure', 'clothes-body', 'cm', 'cmake',
|
||||
'cmb', 'cmd', 'cnet', 'cns', 'cobject', 'cocoa', 'cocor', 'cod4mp', 'code',
|
||||
'code-example', 'codeblock', 'codepoint', 'codimension', 'codstr', 'coffee',
|
||||
'coffeescript', 'coffeescript-preview', 'coil', 'collection', 'collision',
|
||||
'colon', 'colons', 'color', 'color-adjustment', 'coloring', 'colour',
|
||||
'colour-correction', 'colour-interpolation', 'colour-name', 'colour-scheme',
|
||||
'colspan', 'column', 'column-divider', 'column-specials', 'com',
|
||||
'combinators', 'comboboxes', 'comma', 'comma-bar', 'comma-parenthesis',
|
||||
'command', 'command-name', 'command-synopsis', 'commandline', 'commands',
|
||||
'comment', 'comment-ish', 'comment-italic', 'commented-out', 'commit-command',
|
||||
'commit-message', 'commodity', 'common', 'commonform', 'communications',
|
||||
'community', 'commute', 'comnd', 'compare', 'compareOp', 'comparison',
|
||||
'compile', 'compile-only', 'compiled', 'compiled-papyrus', 'compiler',
|
||||
'compiler-directive', 'compiletime', 'compiling-and-loading', 'complement',
|
||||
'complete', 'completed', 'complex', 'component', 'component-separator',
|
||||
'component_instantiation', 'compositor', 'compound', 'compound-assignment',
|
||||
'compress', 'computer', 'computercraft', 'concat', 'concatenated-arguments',
|
||||
'concatenation', 'concatenator', 'concatination', 'concealed', 'concise',
|
||||
'concrete', 'condition', 'conditional', 'conditional-directive',
|
||||
'conditional-short', 'conditionals', 'conditions', 'conf', 'config',
|
||||
'configuration', 'configure', 'confluence', 'conftype', 'conjunction',
|
||||
'conky', 'connect', 'connection-state', 'connectivity', 'connstate', 'cons',
|
||||
'consecutive-tags', 'considering', 'console', 'const', 'const-data',
|
||||
'constant', 'constants', 'constrained', 'constraint', 'constraints',
|
||||
'construct', 'constructor', 'constructor-list', 'constructs', 'consult',
|
||||
'contacts', 'container', 'containers-raycast', 'contains', 'content',
|
||||
'content-detective', 'contentSupplying', 'contentitem', 'context',
|
||||
'context-free', 'context-signature', 'continuation', 'continuations',
|
||||
'continue', 'continued', 'continuum', 'contol', 'contract', 'contracts',
|
||||
'contrl', 'control', 'control-char', 'control-handlers', 'control-management',
|
||||
'control-systems', 'control-transfer', 'controller', 'controlline',
|
||||
'controls', 'contstant', 'conventional', 'conversion', 'convert-type',
|
||||
'cookie', 'cool', 'coord1', 'coord2', 'coord3', 'coordinates', 'copy',
|
||||
'copying', 'coq', 'core', 'core-parse', 'coreutils', 'correct', 'cos',
|
||||
'counter', 'counters', 'cover', 'cplkg', 'cplusplus', 'cpm', 'cpp',
|
||||
'cpp-include', 'cpp-type', 'cpp_type', 'cpu12', 'cql', 'cram', 'crc32',
|
||||
'create', 'creation', 'critic', 'crl', 'crontab', 'crypto', 'crystal', 'cs',
|
||||
'csharp', 'cshtml', 'csi', 'csjs', 'csound', 'csound-document',
|
||||
'csound-score', 'cspm', 'css', 'csv', 'csx', 'ct', 'ctkey', 'ctor', 'ctxvar',
|
||||
'ctxvarbracket', 'ctype', 'cubic-bezier', 'cucumber', 'cuda',
|
||||
'cue-identifier', 'cue-timings', 'cuesheet', 'cup', 'cupsym', 'curl',
|
||||
'curley', 'curly', 'currency', 'current', 'current-escape-char',
|
||||
'curve', 'curve-2d', 'curve-fitting', 'curve-reference', 'curve-technique',
|
||||
'custom', 'customevent', 'cut', 'cve-number', 'cvs', 'cw', 'cxx', 'cy-GB',
|
||||
'cyan', 'cyc', 'cycle', 'cypher', 'cyrix', 'cython', 'd', 'da', 'daml',
|
||||
'dana', 'danger', 'danmakufu', 'dark_aqua', 'dark_blue', 'dark_gray',
|
||||
'dark_green', 'dark_purple', 'dark_red', 'dart', 'dartdoc', 'dash', 'dasm',
|
||||
'data', 'data-acquisition', 'data-extension', 'data-integrity', 'data-item',
|
||||
'data-step', 'data-transfer', 'database', 'database-name', 'datablock',
|
||||
'datablocks', 'datafeed', 'datatype', 'datatypes', 'date', 'date-time',
|
||||
'datetime', 'dav', 'day', 'dayofmonth', 'dayofweek', 'db', 'dba', 'dbx', 'dc',
|
||||
'dcon', 'dd', 'ddp', 'de', 'dealii', 'deallocate', 'deb-control', 'debian',
|
||||
'debris', 'debug', 'debug-specification', 'debugger', 'debugging',
|
||||
'debugging-comment', 'dec', 'decal', 'decimal', 'decimal-arithmetic',
|
||||
'decision', 'decl', 'declaration', 'declaration-expr', 'declaration-prod',
|
||||
'declarations', 'declarator', 'declaratyion', 'declare', 'decode',
|
||||
'decoration', 'decorator', 'decreasing', 'decrement', 'def', 'default',
|
||||
'define', 'define-colour', 'defined', 'definedness', 'definingobj',
|
||||
'definition', 'definitions', 'defintions', 'deflate', 'delay', 'delegated',
|
||||
'delete', 'deleted', 'deletion', 'delimeter', 'delimited', 'delimiter',
|
||||
'delimiter-too-long', 'delimiters', 'dense', 'deprecated', 'depricated',
|
||||
'dereference', 'derived-type', 'deriving', 'desc', 'describe', 'description',
|
||||
'descriptors', 'design', 'desktop', 'destination', 'destructor',
|
||||
'destructured', 'determ', 'developer', 'device', 'device-io', 'dformat', 'dg',
|
||||
'dhcp', 'diagnostic', 'dialogue', 'diamond', 'dict', 'dictionary',
|
||||
'dictionaryname', 'diff', 'difference', 'different', 'diffuse-reflectivity',
|
||||
'digdag', 'digit-width', 'dim', 'dimension', 'dip', 'dir', 'dir-target',
|
||||
'dircolors', 'direct', 'direction', 'directive', 'directive-option',
|
||||
'directives', 'directory', 'dirjs', 'dirtyblue', 'dirtygreen', 'disable',
|
||||
'disable-markdown', 'disable-todo', 'discarded', 'discusson', 'disjunction',
|
||||
'disk', 'disk-folder-file', 'dism', 'displacement', 'display', 'dissolve',
|
||||
'dissolve-interpolation', 'distribution', 'diverging-function', 'divert',
|
||||
'divide', 'divider', 'django', 'dl', 'dlv', 'dm', 'dmf', 'dml', 'do',
|
||||
'dobody', 'doc', 'doc-comment', 'docRoot', 'dockerfile', 'dockerignore',
|
||||
'doconce', 'docstring', 'doctest', 'doctree-option', 'doctype', 'document',
|
||||
'documentation', 'documentroot', 'does', 'dogescript', 'doki', 'dollar',
|
||||
'dollar-quote', 'dollar_variable', 'dom', 'domain', 'dontcollect', 'doors',
|
||||
'dop', 'dot', 'dot-access', 'dotenv', 'dotfiles', 'dothandout', 'dotnet',
|
||||
'dotnote', 'dots', 'dotted', 'dotted-circle', 'dotted-del', 'dotted-greater',
|
||||
'dotted-tack-up', 'double', 'double-arrow', 'double-colon', 'double-dash',
|
||||
'double-dash-not-allowed', 'double-dot', 'double-number-sign',
|
||||
'double-percentage', 'double-qoute', 'double-quote', 'double-quoted',
|
||||
'double-quoted-string', 'double-semicolon', 'double-slash', 'doublequote',
|
||||
'doubleslash', 'dougle', 'down', 'download', 'downwards', 'doxyfile',
|
||||
'doxygen', 'dragdrop', 'drawing', 'drive', 'droiuby', 'drop', 'drop-shadow',
|
||||
'droplevel', 'drummode', 'drupal', 'dsl', 'dsv', 'dt', 'dtl', 'due', 'dummy',
|
||||
'dummy-variable', 'dump', 'duration', 'dust', 'dust_Conditional',
|
||||
'dust_end_section_tag', 'dust_filter', 'dust_partial',
|
||||
'dust_partial_not_self_closing', 'dust_ref', 'dust_ref_name',
|
||||
'dust_section_context', 'dust_section_name', 'dust_section_params',
|
||||
'dust_self_closing_section_tag', 'dust_special', 'dust_start_section_tag',
|
||||
'dustjs', 'dut', 'dwscript', 'dxl', 'dylan', 'dynamic', 'dyndoc', 'dyon', 'e',
|
||||
'e3globals', 'each', 'eachin', 'earl-grey', 'ebnf', 'ebuild', 'echo',
|
||||
'eclass', 'ecmascript', 'eco', 'ecr', 'ect', 'ect2', 'ect3', 'ect4', 'edasm',
|
||||
'edge', 'edit-manager', 'editfields', 'editors', 'ee', 'eex', 'effect',
|
||||
'effectgroup', 'effective_routine_body', 'effects', 'eiffel', 'eight', 'eio',
|
||||
'eiz', 'ejectors', 'el', 'elasticsearch', 'elasticsearch2', 'element',
|
||||
'elements', 'elemnt', 'elif', 'elipse', 'elision', 'elixir', 'ellipsis',
|
||||
'elm', 'elmx', 'else', 'else-condition', 'else-if', 'elseif',
|
||||
'elseif-condition', 'elsewhere', 'eltype', 'elvis', 'em', 'email', 'embed',
|
||||
'embed-diversion', 'embedded', 'embedded-c', 'embedded-ruby', 'embedded2',
|
||||
'embeded', 'ember', 'emberscript', 'emblem', 'embperl', 'emissive-colour',
|
||||
'eml', 'emlist', 'emoji', 'emojicode', 'emp', 'emph', 'emphasis', 'empty',
|
||||
'empty-dictionary', 'empty-list', 'empty-parenthesis', 'empty-start',
|
||||
'empty-string', 'empty-tag', 'empty-tuple', 'empty-typing-pair', 'empty_gif',
|
||||
'emptyelement', 'en', 'en-Scouse', 'en-au', 'en-lol', 'en-old', 'en-pirate',
|
||||
'enable', 'enc', 'enchant', 'enclose', 'encode', 'encoding', 'encryption',
|
||||
'end', 'end-block-data', 'end-definition', 'end-document', 'end-enum',
|
||||
'end-footnote', 'end-of-line', 'end-statement', 'end-value', 'endassociate',
|
||||
'endcode', 'enddo', 'endfile', 'endforall', 'endfunction', 'endian',
|
||||
'endianness', 'endif', 'endinfo', 'ending', 'ending-space', 'endinterface',
|
||||
'endlocaltable', 'endmodule', 'endobject', 'endobjecttable', 'endparamtable',
|
||||
'endprogram', 'endproperty', 'endpropertygroup', 'endpropertygrouptable',
|
||||
'endpropertytable', 'endselect', 'endstate', 'endstatetable', 'endstruct',
|
||||
'endstructtable', 'endsubmodule', 'endsubroutine', 'endtimeblock', 'endtype',
|
||||
'enduserflagsref', 'endvariable', 'endvariabletable', 'endwhere', 'engine',
|
||||
'enterprise', 'entity', 'entity-creation-and-abolishing',
|
||||
'entity_instantiation', 'entry', 'entry-definition', 'entry-key',
|
||||
'entry-type', 'entrypoint', 'enum', 'enum-block', 'enum-declaration',
|
||||
'enumeration', 'enumerator', 'enumerator-specification', 'env', 'environment',
|
||||
'environment-variable', 'eo', 'eof', 'epatch', 'eq', 'eqn', 'eqnarray',
|
||||
'equal', 'equal-or-greater', 'equal-or-less', 'equalexpr', 'equality',
|
||||
'equals', 'equals-sign', 'equation', 'equation-label', 'erb', 'ereg',
|
||||
'erlang', 'error', 'error-control', 'errorfunc', 'errorstop', 'es', 'es6',
|
||||
'es6import', 'esc', 'escape', 'escape-char', 'escape-code', 'escape-sequence',
|
||||
'escape-unicode', 'escaped', 'escapes', 'escript', 'eso-lua', 'eso-txt',
|
||||
'essence', 'et', 'eth', 'ethaddr', 'etml', 'etpl', 'eudoc', 'euler',
|
||||
'euphoria', 'european', 'evaled', 'evaluable', 'evaluation', 'even-tab',
|
||||
'event', 'event-call', 'event-handler', 'event-handling', 'event-schedulling',
|
||||
'eventType', 'eventb', 'eventend', 'events', 'evnd', 'exactly', 'example',
|
||||
'exampleText', 'examples', 'exceeding-sections', 'excel-link', 'exception',
|
||||
'exceptions', 'exclaimation-point', 'exclamation', 'exec', 'exec-command',
|
||||
'execution-context', 'exif', 'existential', 'exit', 'exp', 'expand-register',
|
||||
'expanded', 'expansion', 'expected-array-separator',
|
||||
'expected-dictionary-separator', 'expected-extends', 'expected-implements',
|
||||
'expected-range-separator', 'experimental', 'expires', 'expl3', 'explosion',
|
||||
'exponent', 'exponential', 'export', 'exports', 'expr', 'expression',
|
||||
'expression-separator', 'expression-seperator', 'expressions',
|
||||
'expressions-and-types', 'exprwrap', 'ext', 'extempore', 'extend', 'extended',
|
||||
'extends', 'extension', 'extension-specification', 'extensions', 'extern',
|
||||
'extern-block', 'external', 'external-call', 'external-signature', 'extersk',
|
||||
'extglob', 'extra', 'extra-characters', 'extra-equals-sign', 'extracted',
|
||||
'extras', 'extrassk', 'exxample', 'eztpl', 'f', 'f5networks', 'fa', 'face',
|
||||
'fact', 'factor', 'factorial', 'fadeawayheight', 'fadeawaywidth', 'fail',
|
||||
'fakeroot', 'fallback', 'fallout4', 'false', 'fandoc', 'fann', 'fantom',
|
||||
'fastcgi', 'fbaccidental', 'fbfigure', 'fbgroupclose', 'fbgroupopen', 'fbp',
|
||||
'fctn', 'fe', 'feature', 'features', 'feedrate', 'fenced', 'fftwfn', 'fhem',
|
||||
'fi', 'field', 'field-assignment', 'field-completions', 'field-id',
|
||||
'field-level-comment', 'field-name', 'field-tag', 'fields', 'figbassmode',
|
||||
'figure', 'figuregroup', 'filder-design-hdl-coder', 'file', 'file-i-o',
|
||||
'file-io', 'file-name', 'file-object', 'file-path', 'fileinfo', 'filename',
|
||||
'filepath', 'filetest', 'filter', 'filter-pipe', 'filteredtranscludeblock',
|
||||
'filters', 'final', 'final-procedure', 'finally', 'financial',
|
||||
'financial-derivatives', 'find', 'find-in-files', 'find-m', 'finder',
|
||||
'finish', 'finn', 'fire', 'firebug', 'first', 'first-class', 'first-line',
|
||||
'fish', 'fitnesse', 'five', 'fix_this_later', 'fixed', 'fixed-income',
|
||||
'fixed-point', 'fixme', 'fl', 'flag', 'flag-control', 'flags', 'flash',
|
||||
'flatbuffers', 'flex-config', 'fload', 'float', 'float-exponent', 'float_exp',
|
||||
'floating-point', 'floating_point', 'floor', 'flow', 'flow-control',
|
||||
'flowcontrol', 'flows', 'flowtype', 'flush', 'fma', 'fma4', 'fmod', 'fn',
|
||||
'fold', 'folder', 'folder-actions', 'following', 'font',
|
||||
'font-cache', 'font-face', 'font-name', 'font-size', 'fontface', 'fontforge',
|
||||
'foobar', 'footer', 'footnote', 'for', 'for-in-loop', 'for-loop',
|
||||
'for-quantity', 'forall', 'force', 'foreach', 'foreign', 'forever',
|
||||
'forge-config', 'forin', 'form', 'form-feed', 'formal', 'format',
|
||||
'format-register', 'format-verb', 'formatted', 'formatter', 'formatting',
|
||||
'forth', 'fortran', 'forward', 'foundation', 'fountain', 'four',
|
||||
'fourd-command', 'fourd-constant', 'fourd-constant-hex',
|
||||
'fourd-constant-number', 'fourd-constant-string', 'fourd-control-begin',
|
||||
'fourd-control-end', 'fourd-declaration', 'fourd-declaration-array',
|
||||
'fourd-local-variable', 'fourd-parameter', 'fourd-table', 'fourd-tag',
|
||||
'fourd-variable', 'fpm', 'fpu', 'fpu_x87', 'fr', 'fragment', 'frame',
|
||||
'frames', 'frametitle', 'framexml', 'free', 'free-form', 'freebasic',
|
||||
'freefem', 'freespace2', 'from', 'from-file', 'front-matter', 'fs', 'fs2',
|
||||
'fsc', 'fsgsbase', 'fsharp', 'fsi', 'fsl', 'fsm', 'fsp', 'fsx', 'fth', 'ftl',
|
||||
'ftl20n', 'full-line', 'full-stop', 'fun', 'funarg', 'func-tag', 'func_call',
|
||||
'funchand', 'function', 'function-arity', 'function-attribute',
|
||||
'function-call', 'function-definition', 'function-literal',
|
||||
'function-parameter', 'function-recursive', 'function-return',
|
||||
'function-type', 'functionDeclaration', 'functionDefinition',
|
||||
'function_definition', 'function_prototype', 'functional_test', 'functionend',
|
||||
'functions', 'functionstart', 'fundimental', 'funk', 'funtion-definition',
|
||||
'fus', 'future', 'futures', 'fuzzy-logic', 'fx', 'fx-foliage-replicator',
|
||||
'fx-light', 'fx-shape-replicator', 'fx-sun-light', 'g', 'g-code', 'ga',
|
||||
'gain', 'galaxy', 'gallery', 'game-base', 'game-connection', 'game-server',
|
||||
'gamebusk', 'gamescript', 'gams', 'gams-lst', 'gap', 'garch', 'gather',
|
||||
'gcode', 'gdb', 'gdscript', 'gdx', 'ge', 'geant4-macro', 'geck',
|
||||
'geck-keyword', 'general', 'general-purpose', 'generate', 'generator',
|
||||
'generic', 'generic-config', 'generic-spec', 'generic-type', 'generic_list',
|
||||
'genericcall', 'generics', 'genetic-algorithms', 'geo', 'geometric',
|
||||
'geometry', 'geometry-adjustment', 'get', 'getproperty', 'getsec', 'getset',
|
||||
'getter', 'gettext', 'getword', 'gfm', 'gfm-todotxt', 'gfx', 'gh-number',
|
||||
'gherkin', 'gisdk', 'git', 'git-attributes', 'git-commit', 'git-config',
|
||||
'git-rebase', 'gitignore', 'given', 'gj', 'gl', 'glob', 'global',
|
||||
'global-functions', 'globals', 'globalsection', 'glsl', 'glue',
|
||||
'glyph_class_name', 'glyphname-value', 'gml', 'gmp', 'gmsh', 'gmx', 'gn',
|
||||
'gnu', 'gnuplot', 'go', 'goal', 'goatee', 'godmode', 'gohtml', 'gold', 'golo',
|
||||
'google', 'gosub', 'gotemplate', 'goto', 'goto-label', 'gpd', 'gpd_note',
|
||||
'gpp', 'grace', 'grade-down', 'grade-up', 'gradient', 'gradle', 'grails',
|
||||
'grammar', 'grammar-rule', 'grammar_production', 'grap', 'grapahql', 'graph',
|
||||
'graphics', 'graphql', 'grave-accent', 'gray', 'greater', 'greater-equal',
|
||||
'greater-or-equal', 'greek', 'greek-letter', 'green', 'gremlin', 'grey',
|
||||
'grg', 'grid-table', 'gridlists', 'grog', 'groovy', 'groovy-properties',
|
||||
'group', 'group-level-comment', 'group-name', 'group-number',
|
||||
'group-reference', 'group-title', 'group1', 'group10', 'group11', 'group2',
|
||||
'group3', 'group4', 'group5', 'group6', 'group7', 'group8', 'group9',
|
||||
'groupend', 'groupflag', 'grouping-statement', 'groupname', 'groupstart',
|
||||
'growl', 'grr', 'gs', 'gsc', 'gsp', 'gt', 'guard', 'guards', 'gui',
|
||||
'gui-bitmap-ctrl', 'gui-button-base-ctrl', 'gui-canvas', 'gui-control',
|
||||
'gui-filter-ctrl', 'gui-frameset-ctrl', 'gui-menu-bar',
|
||||
'gui-message-vector-ctrl', 'gui-ml-text-ctrl', 'gui-popup-menu-ctrl',
|
||||
'gui-scroll-ctrl', 'gui-slider-ctrl', 'gui-text-ctrl', 'gui-text-edit-ctrl',
|
||||
'gui-text-list-ctrl', 'guid', 'guillemot', 'guis', 'gzip', 'gzip_static', 'h',
|
||||
'h1', 'hack', 'hackfragment', 'haddock', 'hairpin', 'ham', 'haml', 'hamlbars',
|
||||
'hamlc', 'hamlet', 'hamlpy', 'handlebar', 'handlebars', 'handler',
|
||||
'hanging-paragraph', 'haproxy-config', 'harbou', 'harbour', 'hard-break',
|
||||
'hardlinebreaks', 'hash', 'hash-tick', 'hashbang', 'hashicorp', 'hashkey',
|
||||
'haskell', 'haxe', 'hbs', 'hcl', 'hdl', 'hdr', 'he', 'header',
|
||||
'header-continuation', 'header-value', 'headername', 'headers', 'heading',
|
||||
'heading-0', 'heading-1', 'heading-2', 'heading-3', 'heading-4', 'heading-5',
|
||||
'heading-6', 'height', 'helen', 'help', 'helper', 'helpers', 'heredoc',
|
||||
'heredoc-token', 'herestring', 'heritage', 'hex', 'hex-ascii', 'hex-byte',
|
||||
'hex-literal', 'hex-old', 'hex-string', 'hex-value', 'hex8', 'hexadecimal',
|
||||
'hexidecimal', 'hexprefix', 'hg-commit', 'hgignore', 'hi', 'hidden', 'hide',
|
||||
'high-minus', 'highlight-end', 'highlight-group',
|
||||
'highlight-start', 'hint', 'history', 'hive', 'hive-name', 'hjson', 'hl7',
|
||||
'hlsl', 'hn', 'hoa', 'hoc', 'hocharacter', 'hocomment', 'hocon', 'hoconstant',
|
||||
'hocontinuation', 'hocontrol', 'hombrew-formula', 'homebrew', 'homematic',
|
||||
'hook', 'hoon', 'horizontal-blending', 'horizontal-packed-arithmetic',
|
||||
'horizontal-rule', 'hostname', 'hosts', 'hour', 'hours', 'hps', 'hql', 'hr',
|
||||
'hrm', 'hs', 'hsc2hs', 'ht', 'htaccess', 'htl', 'html', 'html_entity',
|
||||
'htmlbars', 'http', 'hu', 'hungary', 'hxml', 'hy', 'hydrant', 'hydrogen',
|
||||
'hyperbolic', 'hyperlink', 'hyphen', 'hyphenation', 'hyphenation-char', 'i',
|
||||
'i-beam', 'i18n', 'iRev', 'ice', 'icinga2', 'icmc', 'icmptype', 'icmpv6type',
|
||||
'icmpxtype', 'iconv', 'id', 'id-type', 'id-with-protocol', 'idd', 'ideal',
|
||||
'identical', 'identifer', 'identified', 'identifier', 'identifier-type',
|
||||
'identifiers-and-DTDs', 'identity', 'idf', 'idl', 'idris', 'ieee', 'if',
|
||||
'if-block', 'if-branch', 'if-condition', 'if-else', 'if-then', 'ifacespec',
|
||||
'ifdef', 'ifname', 'ifndef', 'ignore', 'ignore-eol', 'ignore-errors',
|
||||
'ignorebii', 'ignored', 'ignored-binding', 'ignoring', 'iisfunc', 'ijk',
|
||||
'ilasm', 'illagal', 'illeagal', 'illegal', 'illumination-model', 'image',
|
||||
'image-acquisition', 'image-alignment', 'image-option', 'image-processing',
|
||||
'images', 'imap', 'imba', 'imfchan', 'img', 'immediate',
|
||||
'immediately-evaluated', 'immutable', 'impex', 'implementation',
|
||||
'implementation-defined-hooks', 'implemented', 'implements', 'implicit',
|
||||
'import', 'import-all', 'importall', 'important', 'in', 'in-block',
|
||||
'in-module', 'in-out', 'inappropriate', 'include', 'include-statement',
|
||||
'includefile', 'incomplete', 'incomplete-variable-assignment', 'inconsistent',
|
||||
'increment', 'increment-decrement', 'indent', 'indented',
|
||||
'indented-paragraph', 'indepimage', 'index', 'index-seperator', 'indexed',
|
||||
'indexer', 'indexes', 'indicator', 'indices', 'indirect', 'indirection',
|
||||
'individual-enum-definition', 'individual-rpc-call', 'inet', 'inetprototype',
|
||||
'inferred', 'infes', 'infinity', 'infix', 'info', 'inform', 'inform6',
|
||||
'inform7', 'infotype', 'ingore-eol', 'inherit', 'inheritDoc', 'inheritance',
|
||||
'inherited', 'inherited-class', 'inherited-struct', 'inherits', 'ini', 'init',
|
||||
'initial-lowercase', 'initial-uppercase', 'initial-value', 'initialization',
|
||||
'initialize', 'initializer-list', 'ink', 'inline', 'inline-data',
|
||||
'inlineConditionalBranchSeparator', 'inlineConditionalClause',
|
||||
'inlineConditionalEnd', 'inlineConditionalStart', 'inlineLogicEnd',
|
||||
'inlineLogicStart', 'inlineSequenceEnd', 'inlineSequenceSeparator',
|
||||
'inlineSequenceStart', 'inlineSequenceTypeChar', 'inlineblock', 'inlinecode',
|
||||
'inlinecomment', 'inlinetag', 'inner', 'inner-class', 'inno', 'ino', 'inout',
|
||||
'input', 'inquire', 'inserted', 'insertion', 'insertion-and-extraction',
|
||||
'inside', 'install', 'instance', 'instancemethods', 'instanceof', 'instances',
|
||||
'instantiation', 'instruction', 'instruction-pointer', 'instructions',
|
||||
'instrument', 'instrument-block', 'instrument-control',
|
||||
'instrument-declaration', 'int', 'int32', 'int64', 'integer', 'integer-float',
|
||||
'intel', 'intel-hex', 'intent', 'intepreted', 'interaction', 'interbase',
|
||||
'interface', 'interface-block', 'interface-or-protocol', 'interfaces',
|
||||
'interior-instance', 'interiors', 'interlink', 'internal', 'internet',
|
||||
'interpolate-argument', 'interpolate-string', 'interpolate-variable',
|
||||
'interpolated', 'interpolation', 'interrupt', 'intersection', 'interval',
|
||||
'intervalOrList', 'intl', 'intrinsic', 'intuicio4', 'invalid',
|
||||
'invalid-character', 'invalid-character-escape', 'invalid-inequality',
|
||||
'invalid-quote', 'invalid-variable-name', 'invariant', 'invocation', 'invoke',
|
||||
'invokee', 'io', 'ior', 'iota', 'ip', 'ip-port', 'ip6', 'ipkg', 'ipsec',
|
||||
'ipv4', 'ipv6', 'ipynb', 'irct', 'irule', 'is', 'isa', 'isc', 'iscexport',
|
||||
'isclass', 'isml', 'issue', 'it', 'italic', 'italic-text', 'item',
|
||||
'item-access', 'itemlevel', 'items', 'iteration', 'itunes', 'ivar', 'ja',
|
||||
'jack', 'jade', 'jakefile', 'jasmin', 'java', 'java-properties', 'java-props',
|
||||
'javadoc', 'javascript', 'jbeam', 'jekyll', 'jflex', 'jibo-rule', 'jinja',
|
||||
'jison', 'jisonlex', 'jmp', 'joint', 'joker', 'jolie', 'jot', 'journaling',
|
||||
'jpl', 'jq', 'jquery', 'js', 'js-label', 'jsdoc', 'jsduck', 'jsim', 'json',
|
||||
'json5', 'jsoniq', 'jsonnet', 'jsont', 'jsp', 'jsx', 'julia', 'julius',
|
||||
'jump', 'juniper', 'juniper-junos-config', 'junit-test-report', 'junos',
|
||||
'juttle', 'jv', 'jxa', 'k', 'kag', 'kagex', 'kb', 'kbd', 'kconfig',
|
||||
'kerboscript', 'kernel', 'kevs', 'kevscript', 'kewyword', 'key',
|
||||
'key-assignment', 'key-letter', 'key-pair', 'key-path', 'key-value',
|
||||
'keyboard', 'keyframe', 'keyframes', 'keygroup', 'keyname', 'keyspace',
|
||||
'keyspace-name', 'keyvalue', 'keyword', 'keyword-parameter', 'keyword1',
|
||||
'keyword2', 'keyword3', 'keyword4', 'keyword5', 'keyword6', 'keyword7',
|
||||
'keyword8', 'keyword_arrays', 'keyword_objects', 'keyword_roots',
|
||||
'keyword_string', 'keywords', 'keywork', 'kickstart', 'kind', 'kmd', 'kn',
|
||||
'knitr', 'knockout', 'knot', 'ko', 'ko-virtual', 'kos', 'kotlin', 'krl',
|
||||
'ksp-cfg', 'kspcfg', 'kurumin', 'kv', 'kxi', 'kxigauge', 'l', 'l20n',
|
||||
'l4proto', 'label', 'label-expression', 'labeled', 'labeled-parameter',
|
||||
'labelled-thing', 'lagda', 'lambda', 'lambda-function', 'lammps', 'langref',
|
||||
'language', 'language-range', 'languagebabel', 'langversion', 'largesk',
|
||||
'lasso', 'last', 'last-paren-match', 'latex', 'latex2', 'latino', 'latte',
|
||||
'launch', 'layout', 'layoutbii', 'lbsearch', 'lc', 'lc-3', 'lcb', 'ldap',
|
||||
'ldif', 'le', 'leader-char', 'leading', 'leading-space', 'leading-tabs',
|
||||
'leaf', 'lean', 'ledger', 'left', 'left-margin', 'leftshift', 'lefttoright',
|
||||
'legacy', 'legacy-setting', 'lemon', 'len', 'length', 'leopard', 'less',
|
||||
'less-equal', 'less-or-equal', 'let', 'letter', 'level', 'level-of-detail',
|
||||
'level1', 'level2', 'level3', 'level4', 'level5', 'level6', 'levels', 'lex',
|
||||
'lexc', 'lexical', 'lf-in-string', 'lhs', 'li', 'lib', 'libfile', 'library',
|
||||
'libs', 'libxml', 'lid', 'lifetime', 'ligature', 'light', 'light_purple',
|
||||
'lighting', 'lightning', 'lilypond', 'lilypond-drummode',
|
||||
'lilypond-figbassmode', 'lilypond-figuregroup', 'lilypond-internals',
|
||||
'lilypond-lyricsmode', 'lilypond-markupmode', 'lilypond-notedrum',
|
||||
'lilypond-notemode', 'lilypond-notemode-explicit', 'lilypond-notenames',
|
||||
'lilypond-schememode', 'limit_zone', 'line-block', 'line-break',
|
||||
'line-continuation', 'line-cue-setting', 'line-statement',
|
||||
'line-too-long', 'linebreak', 'linenumber', 'link', 'link-label',
|
||||
'link-text', 'link-url', 'linkage', 'linkage-type', 'linkedin',
|
||||
'linkedsockets', 'linkplain', 'linkplain-label', 'linq', 'linuxcncgcode',
|
||||
'liquid', 'liquidhaskell', 'liquidsoap', 'lisp', 'lisp-repl', 'list',
|
||||
'list-done', 'list-separator', 'list-style-type', 'list-today', 'list_item',
|
||||
'listing', 'listnum', 'listvalues', 'litaco', 'litcoffee', 'literal',
|
||||
'literal-string', 'literate', 'litword', 'livecodescript', 'livescript',
|
||||
'livescriptscript', 'll', 'llvm', 'load-constants', 'load-hint', 'loader',
|
||||
'local', 'local-variables', 'localhost', 'localizable', 'localized',
|
||||
'localname', 'locals', 'localtable', 'location', 'lock', 'log', 'log-debug',
|
||||
'log-error', 'log-failed', 'log-info', 'log-patch', 'log-success',
|
||||
'log-verbose', 'log-warning', 'logarithm', 'logging', 'logic', 'logicBegin',
|
||||
'logical', 'logical-expression', 'logicblox', 'logicode', 'logo', 'logstash',
|
||||
'logtalk', 'lol', 'long', 'look-ahead', 'look-behind', 'lookahead',
|
||||
'lookaround', 'lookbehind', 'loop', 'loop-control', 'low-high', 'lowercase',
|
||||
'lowercase_character_not_allowed_here', 'lozenge', 'lparen', 'lsg', 'lsl',
|
||||
'lst', 'lst-cpu12', 'lstdo', 'lt', 'lt-gt', 'lterat', 'lu', 'lua', 'lucee',
|
||||
'lucius', 'lury', 'lv', 'lyricsmode', 'm', 'm4', 'm4sh', 'm65816', 'm68k',
|
||||
'mac-classic', 'mac-fsaa', 'machine', 'machineclause', 'macro', 'macro-usage',
|
||||
'macro11', 'macrocallblock', 'macrocallinline', 'madoko', 'magenta', 'magic',
|
||||
'magik', 'mail', 'mailer', 'mailto', 'main', 'makefile', 'makefile2', 'mako',
|
||||
'mamba', 'man', 'mantissa', 'manualmelisma', 'map', 'map-library', 'map-name',
|
||||
'mapfile', 'mapkey', 'mapping', 'mapping-type', 'maprange', 'marasm',
|
||||
'margin', 'marginpar', 'mark', 'mark-input', 'markdown', 'marker', 'marko',
|
||||
'marko-attribute', 'marko-tag', 'markup', 'markupmode', 'mas2j', 'mask',
|
||||
'mason', 'mat', 'mata', 'match', 'match-bind', 'match-branch',
|
||||
'match-condition', 'match-definition', 'match-exception', 'match-option',
|
||||
'match-pattern', 'material', 'material-library', 'material-name', 'math',
|
||||
'math-symbol', 'math_complex', 'math_real', 'mathematic', 'mathematica',
|
||||
'mathematical', 'mathematical-symbols', 'mathematics', 'mathjax', 'mathml',
|
||||
'matlab', 'matrix', 'maude', 'maven', 'max', 'max-angle', 'max-distance',
|
||||
'max-length', 'maxscript', 'maybe', 'mb', 'mbstring', 'mc', 'mcc', 'mccolor',
|
||||
'mch', 'mcn', 'mcode', 'mcq', 'mcr', 'mcrypt', 'mcs', 'md', 'mdash', 'mdoc',
|
||||
'mdx', 'me', 'measure', 'media', 'media-feature', 'media-property',
|
||||
'media-type', 'mediawiki', 'mei', 'mel', 'memaddress', 'member',
|
||||
'member-function-attribute', 'member-of', 'membership', 'memcache',
|
||||
'memcached', 'memoir', 'memoir-alltt', 'memoir-fbox', 'memoir-verbatim',
|
||||
'memory', 'memory-management', 'memory-protection', 'memos', 'menhir',
|
||||
'mention', 'menu', 'mercury', 'merge-group', 'merge-key', 'merlin',
|
||||
'mesgTrigger', 'mesgType', 'message', 'message-declaration',
|
||||
'message-forwarding-handler', 'message-sending', 'message-vector', 'messages',
|
||||
'meta', 'meta-conditional', 'meta-data', 'meta-file', 'meta-info',
|
||||
'metaclass', 'metacommand', 'metadata', 'metakey', 'metamodel', 'metapost',
|
||||
'metascript', 'meteor', 'method', 'method-call', 'method-definition',
|
||||
'method-modification', 'method-mofification', 'method-parameter',
|
||||
'method-parameters', 'method-restriction', 'methodcalls', 'methods',
|
||||
'metrics', 'mhash', 'microsites', 'microsoft-dynamics', 'middle',
|
||||
'midi_processing', 'migration', 'mime', 'min', 'minelua', 'minetweaker',
|
||||
'minitemplate', 'minitest', 'minus', 'minute', 'mips', 'mirah', 'misc',
|
||||
'miscellaneous', 'mismatched', 'missing', 'missing-asterisk',
|
||||
'missing-inheritance', 'missing-parameters', 'missing-section-begin',
|
||||
'missingend', 'mission-area', 'mixin', 'mixin-name', 'mjml', 'ml', 'mlab',
|
||||
'mls', 'mm', 'mml', 'mmx', 'mmx_instructions', 'mn', 'mnemonic',
|
||||
'mobile-messaging', 'mochi', 'mod', 'mod-r', 'mod_perl', 'mod_perl_1',
|
||||
'modblock', 'modbus', 'mode', 'model', 'model-based-calibration',
|
||||
'model-predictive-control', 'modelica', 'modelicascript', 'modeline',
|
||||
'models', 'modern', 'modified', 'modifier', 'modifiers', 'modify',
|
||||
'modify-range', 'modifytime', 'modl', 'modr', 'modula-2', 'module',
|
||||
'module-alias', 'module-binding', 'module-definition', 'module-expression',
|
||||
'module-function', 'module-reference', 'module-rename', 'module-sum',
|
||||
'module-type', 'module-type-definition', 'modules', 'modulo', 'modx',
|
||||
'mojolicious', 'mojom', 'moment', 'mond', 'money', 'mongo', 'mongodb',
|
||||
'monicelli', 'monitor', 'monkberry', 'monkey', 'monospace', 'monospaced',
|
||||
'monte', 'month', 'moon', 'moonscript', 'moos', 'moose', 'moosecpp', 'motion',
|
||||
'mouse', 'mov', 'movement', 'movie', 'movie-file', 'mozu', 'mpw', 'mpx',
|
||||
'mqsc', 'ms', 'mscgen', 'mscript', 'msg', 'msgctxt', 'msgenny', 'msgid',
|
||||
'msgstr', 'mson', 'mson-block', 'mss', 'mta', 'mtl', 'mucow', 'mult', 'multi',
|
||||
'multi-line', 'multi-symbol', 'multi-threading', 'multiclet', 'multids-file',
|
||||
'multiline', 'multiline-cell', 'multiline-text-reference',
|
||||
'multiline-tiddler-title', 'multimethod', 'multipart', 'multiplication',
|
||||
'multiplicative', 'multiply', 'multiverse', 'mumps', 'mundosk', 'music',
|
||||
'must_be', 'mustache', 'mut', 'mutable', 'mutator', 'mx', 'mxml', 'mydsl1',
|
||||
'mylanguage', 'mysql', 'mysqli', 'mysqlnd-memcache', 'mysqlnd-ms',
|
||||
'mysqlnd-qc', 'mysqlnd-uh', 'mzn', 'nabla', 'nagios', 'name', 'name-list',
|
||||
'name-of-parameter', 'named', 'named-char', 'named-key', 'named-tuple',
|
||||
'nameless-typed', 'namelist', 'names', 'namespace', 'namespace-block',
|
||||
'namespace-definition', 'namespace-language', 'namespace-prefix',
|
||||
'namespace-reference', 'namespace-statement', 'namespaces', 'nan', 'nand',
|
||||
'nant', 'nant-build', 'narration', 'nas', 'nasal', 'nasl', 'nasm', 'nastran',
|
||||
'nat', 'native', 'nativeint', 'natural', 'navigation', 'nbtkey', 'ncf', 'ncl',
|
||||
'ndash', 'ne', 'nearley', 'neg-ratio', 'negatable', 'negate', 'negated',
|
||||
'negation', 'negative', 'negative-look-ahead', 'negative-look-behind',
|
||||
'negativity', 'nesc', 'nessuskb', 'nested', 'nested_braces',
|
||||
'nested_brackets', 'nested_ltgt', 'nested_parens', 'nesty', 'net',
|
||||
'net-object', 'netbios', 'network', 'network-value', 'networking',
|
||||
'neural-network', 'new', 'new-line', 'new-object', 'newline',
|
||||
'newline-spacing', 'newlinetext', 'newlisp', 'newobject', 'nez', 'nft',
|
||||
'ngdoc', 'nginx', 'nickname', 'nil', 'nim', 'nine', 'ninja', 'ninjaforce',
|
||||
'nit', 'nitro', 'nix', 'nl', 'nlf', 'nm', 'nm7', 'no', 'no-capture',
|
||||
'no-completions', 'no-content', 'no-default', 'no-indent',
|
||||
'no-leading-digits', 'no-trailing-digits', 'no-validate-params', 'node',
|
||||
'nogc', 'noindent', 'nokia-sros-config', 'non', 'non-capturing',
|
||||
'non-immediate', 'non-null-typehinted', 'non-standard', 'non-terminal',
|
||||
'nondir-target', 'none', 'none-parameter', 'nonlocal', 'nonterminal', 'noon',
|
||||
'noop', 'nop', 'noparams', 'nor', 'normal', 'normal_numeric',
|
||||
'normal_objects', 'normal_text', 'normalised', 'not', 'not-a-number',
|
||||
'not-equal', 'not-identical', 'notation', 'note', 'notechord', 'notemode',
|
||||
'notequal', 'notequalexpr', 'notes', 'notidentical', 'notification', 'nowdoc',
|
||||
'noweb', 'nrtdrv', 'nsapi', 'nscript', 'nse', 'nsis', 'nsl', 'ntriples',
|
||||
'nul', 'null', 'nullify', 'nullological', 'nulltype', 'num', 'number',
|
||||
'number-sign', 'number-sign-equals', 'numbered', 'numberic', 'numbers',
|
||||
'numbersign', 'numeric', 'numeric_std', 'numerical', 'nunjucks', 'nut',
|
||||
'nvatom', 'nxc', 'o', 'obj', 'objaggregation', 'objc', 'objcpp', 'objdump',
|
||||
'object', 'object-comments', 'object-definition', 'object-level-comment',
|
||||
'object-name', 'objects', 'objectset', 'objecttable', 'objectvalues', 'objj',
|
||||
'obsolete', 'ocaml', 'ocamllex', 'occam', 'oci8', 'ocmal', 'oct', 'octal',
|
||||
'octave', 'octave-change', 'octave-shift', 'octet', 'octo', 'octobercms',
|
||||
'octothorpe', 'odd-tab', 'odedsl', 'ods', 'of', 'off', 'offset', 'ofx',
|
||||
'ogre', 'ok', 'ol', 'old', 'old-style', 'omap', 'omitted', 'on-background',
|
||||
'on-error', 'once', 'one', 'one-sixth-em', 'one-twelfth-em', 'oniguruma',
|
||||
'oniguruma-comment', 'only', 'only-in', 'onoff', 'ooc', 'oot', 'op-domain',
|
||||
'op-range', 'opa', 'opaque', 'opc', 'opcache', 'opcode',
|
||||
'opcode-argument-types', 'opcode-declaration', 'opcode-definition',
|
||||
'opcode-details', 'open', 'open-gl', 'openal', 'openbinding', 'opencl',
|
||||
'opendss', 'opening', 'opening-text', 'openmp', 'openssl', 'opentype',
|
||||
'operand', 'operands', 'operation', 'operator', 'operator2', 'operator3',
|
||||
'operators', 'opmask', 'opmaskregs', 'optical-density', 'optimization',
|
||||
'option', 'option-description', 'option-toggle', 'optional',
|
||||
'optional-parameter', 'optional-parameter-assignment', 'optionals',
|
||||
'optionname', 'options', 'optiontype', 'or', 'oracle', 'orbbasic', 'orcam',
|
||||
'orchestra', 'order', 'ordered', 'ordered-block', 'ordinal', 'organized',
|
||||
'orgtype', 'origin', 'osiris', 'other', 'other-inherited-class',
|
||||
'other_buildins', 'other_keywords', 'others', 'otherwise',
|
||||
'otherwise-expression', 'out', 'outer', 'output', 'overload', 'override',
|
||||
'owner', 'ownership', 'oz', 'p', 'p4', 'p5', 'p8', 'pa', 'package',
|
||||
'package-definition', 'package_body', 'packages', 'packed',
|
||||
'packed-arithmetic', 'packed-blending', 'packed-comparison',
|
||||
'packed-conversion', 'packed-floating-point', 'packed-integer', 'packed-math',
|
||||
'packed-mov', 'packed-other', 'packed-shift', 'packed-shuffle', 'packed-test',
|
||||
'padlock', 'page', 'page-props', 'pagebreak', 'pair', 'pair-programming',
|
||||
'paket', 'pandoc', 'papyrus', 'papyrus-assembly', 'paragraph', 'parallel',
|
||||
'param', 'param-list', 'paramater', 'paramerised-type', 'parameter',
|
||||
'parameter-entity', 'parameter-space', 'parameterless', 'parameters',
|
||||
'paramless', 'params', 'paramtable', 'paramter', 'paren', 'paren-group',
|
||||
'parens', 'parent', 'parent-reference', 'parent-selector',
|
||||
'parent-selector-suffix', 'parenthases', 'parentheses', 'parenthesis',
|
||||
'parenthetical', 'parenthetical_list', 'parenthetical_pair', 'parfor',
|
||||
'parfor-quantity', 'parse', 'parsed', 'parser', 'parser-function',
|
||||
'parser-token', 'parser3', 'part', 'partial', 'particle', 'pascal', 'pass',
|
||||
'pass-through', 'passive', 'passthrough', 'password', 'password-hash',
|
||||
'patch', 'path', 'path-camera', 'path-pattern', 'pathoperation', 'paths',
|
||||
'pathspec', 'patientId', 'pattern', 'pattern-argument', 'pattern-binding',
|
||||
'pattern-definition', 'pattern-match', 'pattern-offset', 'patterns', 'pause',
|
||||
'payee', 'payload', 'pbo', 'pbtxt', 'pcdata', 'pcntl', 'pdd', 'pddl', 'ped',
|
||||
'pegcoffee', 'pegjs', 'pending', 'percentage', 'percentage-sign',
|
||||
'percussionnote', 'period', 'perl', 'perl-section', 'perl6', 'perl6fe',
|
||||
'perlfe', 'perlt6e', 'perm', 'permutations', 'personalization', 'pervasive',
|
||||
'pf', 'pflotran', 'pfm', 'pfx', 'pgn', 'pgsql', 'phone', 'phone-number',
|
||||
'phonix', 'php', 'php-code-in-comment', 'php_apache', 'php_dom', 'php_ftp',
|
||||
'php_imap', 'php_mssql', 'php_odbc', 'php_pcre', 'php_spl', 'php_zip',
|
||||
'phpdoc', 'phrasemodifiers', 'phraslur', 'physical-zone', 'physics', 'pi',
|
||||
'pic', 'pick', 'pickup', 'picture', 'pig', 'pillar', 'pipe', 'pipe-sign',
|
||||
'pipeline', 'piratesk', 'pitch', 'pixie', 'pkgbuild', 'pl', 'placeholder',
|
||||
'placeholder-parts', 'plain', 'plainsimple-emphasize', 'plainsimple-heading',
|
||||
'plainsimple-number', 'plantuml', 'player', 'playerversion', 'pld_modeling',
|
||||
'please-build', 'please-build-defs', 'plist', 'plsql', 'plugin', 'plus',
|
||||
'plztarget', 'pmc', 'pml', 'pmlPhysics-arrangecharacter',
|
||||
'pmlPhysics-emphasisequote', 'pmlPhysics-graphic', 'pmlPhysics-header',
|
||||
'pmlPhysics-htmlencoded', 'pmlPhysics-links', 'pmlPhysics-listtable',
|
||||
'pmlPhysics-physicalquantity', 'pmlPhysics-relationships',
|
||||
'pmlPhysics-slides', 'pmlPhysics-slidestacks', 'pmlPhysics-speech',
|
||||
'pmlPhysics-structure', 'pnt', 'po', 'pod', 'poe', 'pogoscript', 'point',
|
||||
'point-size', 'pointer', 'pointer-arith', 'pointer-following', 'points',
|
||||
'polarcoord', 'policiesbii', 'policy', 'polydelim', 'polygonal', 'polymer',
|
||||
'polymorphic', 'polymorphic-variant', 'polynomial-degree', 'polysep', 'pony',
|
||||
'port', 'port_list', 'pos-ratio', 'position-cue-setting', 'positional',
|
||||
'positive', 'posix', 'posix-reserved', 'post-match', 'postblit', 'postcss',
|
||||
'postfix', 'postpone', 'postscript', 'potigol', 'potion', 'pound',
|
||||
'pound-sign', 'povray', 'power', 'power_set', 'powershell', 'pp', 'ppc',
|
||||
'ppcasm', 'ppd', 'praat', 'pragma', 'pragma-all-once', 'pragma-mark',
|
||||
'pragma-message', 'pragma-newline-spacing', 'pragma-newline-spacing-value',
|
||||
'pragma-once', 'pragma-stg', 'pragma-stg-value', 'pre', 'pre-defined',
|
||||
'pre-match', 'preamble', 'prec', 'precedence', 'precipitation', 'precision',
|
||||
'precision-point', 'pred', 'predefined', 'predicate', 'prefetch',
|
||||
'prefetchwt', 'prefix', 'prefixed-uri', 'prefixes', 'preinst', 'prelude',
|
||||
'prepare', 'prepocessor', 'preposition', 'prepositional', 'preprocessor',
|
||||
'prerequisites', 'preset', 'preview', 'previous', 'prg', 'primary',
|
||||
'primitive', 'primitive-datatypes', 'primitive-field', 'print',
|
||||
'print-argument', 'priority', 'prism', 'private', 'privileged', 'pro',
|
||||
'probe', 'proc', 'procedure', 'procedure_definition', 'procedure_prototype',
|
||||
'process', 'process-id', 'process-substitution', 'processes', 'processing',
|
||||
'proctitle', 'production', 'profile', 'profiling', 'program', 'program-block',
|
||||
'program-name', 'progressbars', 'proguard', 'project', 'projectile', 'prolog',
|
||||
'prolog-flags', 'prologue', 'promoted', 'prompt', 'prompt-prefix', 'prop',
|
||||
'properties', 'properties_literal', 'property', 'property-flag',
|
||||
'property-list', 'property-name', 'property-value',
|
||||
'property-with-attributes', 'propertydef', 'propertyend', 'propertygroup',
|
||||
'propertygrouptable', 'propertyset', 'propertytable', 'proposition',
|
||||
'protection', 'protections', 'proto', 'protobuf', 'protobufs', 'protocol',
|
||||
'protocol-specification', 'prototype', 'provision', 'proxy', 'psci', 'pseudo',
|
||||
'pseudo-class', 'pseudo-element', 'pseudo-method', 'pseudo-mnemonic',
|
||||
'pseudo-variable', 'pshdl', 'pspell', 'psql', 'pt', 'ptc-config',
|
||||
'ptc-config-modelcheck', 'pthread', 'ptr', 'ptx', 'public', 'pug',
|
||||
'punchcard', 'punctual', 'punctuation', 'punctutation', 'puncuation',
|
||||
'puncutation', 'puntuation', 'puppet', 'purebasic', 'purescript', 'pweave',
|
||||
'pwisa', 'pwn', 'py2pml', 'pyj', 'pyjade', 'pymol', 'pyresttest', 'python',
|
||||
'python-function', 'q', 'q-brace', 'q-bracket', 'q-ltgt', 'q-paren', 'qa',
|
||||
'qm', 'qml', 'qos', 'qoute', 'qq', 'qq-brace', 'qq-bracket', 'qq-ltgt',
|
||||
'qq-paren', 'qry', 'qtpro', 'quad', 'quad-arrow-down', 'quad-arrow-left',
|
||||
'quad-arrow-right', 'quad-arrow-up', 'quad-backslash', 'quad-caret-down',
|
||||
'quad-caret-up', 'quad-circle', 'quad-colon', 'quad-del-down', 'quad-del-up',
|
||||
'quad-diamond', 'quad-divide', 'quad-equal', 'quad-jot', 'quad-less',
|
||||
'quad-not-equal', 'quad-question', 'quad-quote', 'quad-slash', 'quadrigraph',
|
||||
'qual', 'qualified', 'qualifier', 'quality', 'quant', 'quantifier',
|
||||
'quantifiers', 'quartz', 'quasi', 'quasiquote', 'quasiquotes', 'query',
|
||||
'query-dsl', 'question', 'questionmark', 'quicel', 'quicktemplate',
|
||||
'quicktime-file', 'quotation', 'quote', 'quoted', 'quoted-identifier',
|
||||
'quoted-object', 'quoted-or-unquoted', 'quotes', 'qx', 'qx-brace',
|
||||
'qx-bracket', 'qx-ltgt', 'qx-paren', 'r', 'r3', 'rabl', 'racket', 'radar',
|
||||
'radar-area', 'radiobuttons', 'radix', 'rails', 'rainmeter', 'raml', 'random',
|
||||
'random_number', 'randomsk', 'range', 'range-2', 'rank', 'rant', 'rapid',
|
||||
'rarity', 'ratio', 'rational-form', 'raw', 'raw-regex', 'raxe', 'rb', 'rd',
|
||||
'rdfs-type', 'rdrand', 'rdseed', 'react', 'read', 'readline', 'readonly',
|
||||
'readwrite', 'real', 'realip', 'rebeca', 'rebol', 'rec', 'receive',
|
||||
'receive-channel', 'recipe', 'recipient-subscriber-list', 'recode', 'record',
|
||||
'record-field', 'record-usage', 'recordfield', 'recutils', 'red',
|
||||
'redbook-audio', 'redirect', 'redirection', 'redprl', 'redundancy', 'ref',
|
||||
'refer', 'reference', 'referer', 'refinement', 'reflection', 'reg', 'regex',
|
||||
'regexname', 'regexp', 'regexp-option', 'region-anchor-setting',
|
||||
'region-cue-setting', 'region-identifier-setting', 'region-lines-setting',
|
||||
'region-scroll-setting', 'region-viewport-anchor-setting',
|
||||
'region-width-setting', 'register', 'register-64', 'registers', 'regular',
|
||||
'reiny', 'reject', 'rejecttype', 'rel', 'related', 'relation', 'relational',
|
||||
'relations', 'relationship', 'relationship-name', 'relationship-pattern',
|
||||
'relationship-pattern-end', 'relationship-pattern-start', 'relationship-type',
|
||||
'relationship-type-or', 'relationship-type-ored', 'relationship-type-start',
|
||||
'relative', 'rem', 'reminder', 'remote', 'removed', 'rename', 'renamed-from',
|
||||
'renamed-to', 'renaming', 'render', 'renpy', 'reocrd', 'reparator', 'repeat',
|
||||
'repl-prompt', 'replace', 'replaceXXX', 'replaced', 'replacement', 'reply',
|
||||
'repo', 'reporter', 'reporting', 'repository', 'request', 'request-type',
|
||||
'require', 'required', 'requiredness', 'requirement', 'requirements',
|
||||
'rescue', 'reserved', 'reset', 'resolution', 'resource', 'resource-manager',
|
||||
'response', 'response-type', 'rest', 'rest-args', 'rester', 'restriced',
|
||||
'restructuredtext', 'result', 'result-separator', 'results', 'retro',
|
||||
'return', 'return-type', 'return-value', 'returns', 'rev', 'reverse',
|
||||
'reversed', 'review', 'rewrite', 'rewrite-condition', 'rewrite-operator',
|
||||
'rewrite-pattern', 'rewrite-substitution', 'rewrite-test', 'rewritecond',
|
||||
'rewriterule', 'rf', 'rfc', 'rgb', 'rgb-percentage', 'rgb-value', 'rhap',
|
||||
'rho', 'rhs', 'rhtml', 'richtext', 'rid', 'right', 'ring', 'riot',
|
||||
'rivescript', 'rjs', 'rl', 'rmarkdown', 'rnc', 'rng', 'ro', 'roboconf',
|
||||
'robot', 'robotc', 'robust-control', 'rockerfile', 'roff', 'role',
|
||||
'rollout-control', 'root', 'rotate', 'rotate-first', 'rotate-last', 'round',
|
||||
'round-brackets', 'router', 'routeros', 'routes', 'routine', 'row', 'row2',
|
||||
'rowspan', 'roxygen', 'rparent', 'rpc', 'rpc-definition', 'rpe', 'rpm-spec',
|
||||
'rpmspec', 'rpt', 'rq', 'rrd', 'rsl', 'rspec', 'rtemplate', 'ru', 'ruby',
|
||||
'rubymotion', 'rule', 'rule-identifier', 'rule-name', 'rule-pattern',
|
||||
'rule-tag', 'ruleDefinition', 'rules', 'run', 'rune', 'runoff', 'runtime',
|
||||
'rust', 'rviz', 'rx', 's', 'safe-call', 'safe-navigation', 'safe-trap',
|
||||
'safer', 'safety', 'sage', 'salesforce', 'salt', 'sampler',
|
||||
'sampler-comparison', 'samplerarg', 'sampling', 'sas', 'sass',
|
||||
'sass-script-maps', 'satcom', 'satisfies', 'sblock', 'scad', 'scala',
|
||||
'scaladoc', 'scalar', 'scale', 'scam', 'scan', 'scenario', 'scenario_outline',
|
||||
'scene', 'scene-object', 'scheduled', 'schelp', 'schem', 'schema', 'scheme',
|
||||
'schememode', 'scientific', 'scilab', 'sck', 'scl', 'scope', 'scope-name',
|
||||
'scope-resolution', 'scoping', 'score', 'screen', 'scribble', 'script',
|
||||
'script-flag', 'script-metadata', 'script-object', 'script-tag', 'scripting',
|
||||
'scriptlet', 'scriptlocal', 'scriptname', 'scriptname-declaration', 'scripts',
|
||||
'scroll', 'scrollbars', 'scrollpanes', 'scss', 'scumm', 'sdbl', 'sdl', 'sdo',
|
||||
'sealed', 'search', 'seawolf', 'second', 'secondary', 'section',
|
||||
'section-attribute', 'sectionname', 'sections', 'see', 'segment',
|
||||
'segment-registers', 'segment-resolution', 'select', 'select-block',
|
||||
'selector', 'self', 'self-binding', 'self-close', 'sem', 'semantic',
|
||||
'semanticmodel', 'semi-colon', 'semicolon', 'semicoron', 'semireserved',
|
||||
'send-channel', 'sender', 'senum', 'sep', 'separator', 'separatory',
|
||||
'sepatator', 'seperator', 'sequence', 'sequences', 'serial', 'serpent',
|
||||
'server', 'service', 'service-declaration', 'service-rpc', 'services',
|
||||
'session', 'set', 'set-colour', 'set-size', 'set-variable', 'setbagmix',
|
||||
'setname', 'setproperty', 'sets', 'setter', 'setting', 'settings', 'settype',
|
||||
'setword', 'seven', 'severity', 'sexpr', 'sfd', 'sfst', 'sgml', 'sgx1',
|
||||
'sgx2', 'sha', 'sha256', 'sha512', 'sha_functions', 'shad', 'shade',
|
||||
'shaderlab', 'shadow-object', 'shape', 'shape-base', 'shape-base-data',
|
||||
'shared', 'shared-static', 'sharp', 'sharpequal', 'sharpge', 'sharpgt',
|
||||
'sharple', 'sharplt', 'sharpness', 'shebang', 'shell', 'shell-function',
|
||||
'shell-session', 'shift', 'shift-and-rotate', 'shift-left', 'shift-right',
|
||||
'shine', 'shinescript', 'shipflow', 'shmop', 'short', 'shortcut', 'shortcuts',
|
||||
'shorthand', 'shorthandpropertyname', 'show', 'show-argument',
|
||||
'shuffle-and-unpack', 'shutdown', 'shy', 'sidebar', 'sifu', 'sigdec', 'sigil',
|
||||
'sign-line', 'signal', 'signal-processing', 'signature', 'signed',
|
||||
'signed-int', 'signedness', 'signifier', 'silent', 'sim-group', 'sim-object',
|
||||
'sim-set', 'simd', 'simd-horizontal', 'simd-integer', 'simple',
|
||||
'simple-delimiter', 'simple-divider', 'simple-element', 'simple_delimiter',
|
||||
'simplexml', 'simplez', 'simulate', 'since', 'singe', 'single', 'single-line',
|
||||
'single-quote', 'single-quoted', 'single_quote', 'singlequote', 'singleton',
|
||||
'singleword', 'sites', 'six', 'size', 'size-cue-setting', 'sized_integer',
|
||||
'sizeof', 'sjs', 'sjson', 'sk', 'skaction', 'skdragon', 'skeeland',
|
||||
'skellett', 'sketchplugin', 'skevolved', 'skew', 'skill', 'skipped',
|
||||
'skmorkaz', 'skquery', 'skrambled', 'skrayfall', 'skript', 'skrpg', 'sksharp',
|
||||
'skstuff', 'skutilities', 'skvoice', 'sky', 'skyrim', 'sl', 'slash',
|
||||
'slash-bar', 'slash-option', 'slash-sign', 'slashes', 'sleet', 'slice',
|
||||
'slim', 'slm', 'sln', 'slot', 'slugignore', 'sma', 'smali', 'smalltalk',
|
||||
'smarty', 'smb', 'smbinternal', 'smilebasic', 'sml', 'smoothing-group',
|
||||
'smpte', 'smtlib', 'smx', 'snakeskin', 'snapshot', 'snlog', 'snmp', 'so',
|
||||
'soap', 'social', 'socketgroup', 'sockets', 'soft', 'solidity', 'solve',
|
||||
'soma', 'somearg', 'something', 'soql', 'sort', 'sorting', 'souce', 'sound',
|
||||
'sound_processing', 'sound_synthesys', 'source', 'source-constant', 'soy',
|
||||
'sp', 'space', 'space-after-command', 'spacebars', 'spaces', 'sparql',
|
||||
'spath', 'spec', 'special', 'special-attributes', 'special-character',
|
||||
'special-curve', 'special-functions', 'special-hook', 'special-keyword',
|
||||
'special-method', 'special-point', 'special-token-sequence', 'special-tokens',
|
||||
'special-type', 'specification', 'specifier', 'spectral-curve',
|
||||
'specular-exponent', 'specular-reflectivity', 'sphinx', 'sphinx-domain',
|
||||
'spice', 'spider', 'spindlespeed', 'splat', 'spline', 'splunk', 'splunk-conf',
|
||||
'splus', 'spn', 'spread', 'spread-line', 'spreadmap', 'sprite', 'sproto',
|
||||
'sproutcore', 'sqf', 'sql', 'sqlbuiltin', 'sqlite', 'sqlsrv', 'sqr', 'sqsp',
|
||||
'squad', 'square', 'squart', 'squirrel', 'sr-Cyrl', 'sr-Latn', 'src',
|
||||
'srltext', 'sros', 'srt', 'srv', 'ss', 'ssa', 'sse', 'sse2', 'sse2_simd',
|
||||
'sse3', 'sse4', 'sse4_simd', 'sse5', 'sse_avx', 'sse_simd', 'ssh-config',
|
||||
'ssi', 'ssl', 'ssn', 'sstemplate', 'st', 'stable', 'stack', 'stack-effect',
|
||||
'stackframe', 'stage', 'stan', 'standard', 'standard-key', 'standard-links',
|
||||
'standard-suite', 'standardadditions', 'standoc', 'star', 'starline', 'start',
|
||||
'start-block', 'start-condition', 'start-symbol', 'start-value',
|
||||
'starting-function-params', 'starting-functions', 'starting-functions-point',
|
||||
'startshape', 'stata', 'statamic', 'state', 'state-flag', 'state-management',
|
||||
'stateend', 'stategrouparg', 'stategroupval', 'statement',
|
||||
'statement-separator', 'states', 'statestart', 'statetable', 'static',
|
||||
'static-assert', 'static-classes', 'static-if', 'static-shape',
|
||||
'staticimages', 'statistics', 'stats', 'std', 'stdWrap', 'std_logic',
|
||||
'std_logic_1164', 'stderr-write-file', 'stdint', 'stdlib', 'stdlibcall',
|
||||
'stdplugin', 'stem', 'step', 'step-size', 'steps', 'stg', 'stile-shoe-left',
|
||||
'stile-shoe-up', 'stile-tilde', 'stitch', 'stk', 'stmt', 'stochastic', 'stop',
|
||||
'stopping', 'storage', 'story', 'stp', 'straight-quote', 'stray',
|
||||
'stray-comment-end', 'stream', 'stream-selection-and-control', 'streamsfuncs',
|
||||
'streem', 'strict', 'strictness', 'strike', 'strikethrough', 'string',
|
||||
'string-constant', 'string-format', 'string-interpolation',
|
||||
'string-long-quote', 'string-long-single-quote', 'string-single-quote',
|
||||
'stringchar', 'stringize', 'strings', 'strong', 'struc', 'struct',
|
||||
'struct-union-block', 'structdef', 'structend', 'structs', 'structstart',
|
||||
'structtable', 'structure', 'stuff', 'stupid-goddamn-hack', 'style',
|
||||
'styleblock', 'styles', 'stylus', 'sub', 'sub-pattern', 'subchord', 'subckt',
|
||||
'subcmd', 'subexp', 'subexpression', 'subkey', 'subkeys', 'subl', 'submodule',
|
||||
'subnet', 'subnet6', 'subpattern', 'subprogram', 'subroutine', 'subscript',
|
||||
'subsection', 'subsections', 'subset', 'subshell', 'subsort', 'substituted',
|
||||
'substitution', 'substitution-definition', 'subtitle', 'subtlegradient',
|
||||
'subtlegray', 'subtract', 'subtraction', 'subtype', 'suffix', 'sugarml',
|
||||
'sugarss', 'sugly', 'sugly-comparison-operators', 'sugly-control-keywords',
|
||||
'sugly-declare-function', 'sugly-delcare-operator', 'sugly-delcare-variable',
|
||||
'sugly-else-in-invalid-position', 'sugly-encode-clause',
|
||||
'sugly-function-groups', 'sugly-function-recursion',
|
||||
'sugly-function-variables', 'sugly-general-functions',
|
||||
'sugly-general-operators', 'sugly-generic-classes', 'sugly-generic-types',
|
||||
'sugly-global-function', 'sugly-int-constants', 'sugly-invoke-function',
|
||||
'sugly-json-clause', 'sugly-language-constants', 'sugly-math-clause',
|
||||
'sugly-math-constants', 'sugly-multiple-parameter-function',
|
||||
'sugly-number-constants', 'sugly-operator-operands', 'sugly-print-clause',
|
||||
'sugly-single-parameter-function', 'sugly-subject-or-predicate',
|
||||
'sugly-type-function', 'sugly-uri-clause', 'summary', 'super', 'superclass',
|
||||
'supercollider', 'superscript', 'superset', 'supervisor', 'supervisord',
|
||||
'supplemental', 'supplimental', 'support', 'suppress-image-or-category',
|
||||
'suppressed', 'surface', 'surface-technique', 'sv', 'svg', 'svm', 'svn',
|
||||
'swift', 'swig', 'switch', 'switch-block', 'switch-expression',
|
||||
'switch-statement', 'switchEnd', 'switchStart', 'swizzle', 'sybase',
|
||||
'syllableseparator', 'symbol', 'symbol-definition', 'symbol-type', 'symbolic',
|
||||
'symbolic-math', 'symbols', 'symmetry', 'sync-match', 'sync-mode',
|
||||
'sync-mode-location', 'synchronization', 'synchronize', 'synchronized',
|
||||
'synergy', 'synopsis', 'syntax', 'syntax-case', 'syntax-cluster',
|
||||
'syntax-conceal', 'syntax-error', 'syntax-include', 'syntax-item',
|
||||
'syntax-keywords', 'syntax-match', 'syntax-option', 'syntax-region',
|
||||
'syntax-rule', 'syntax-spellcheck', 'syntax-sync', 'sys-types', 'sysj',
|
||||
'syslink', 'syslog-ng', 'system', 'system-events', 'system-identification',
|
||||
'system-table-pointer', 'systemreference', 'sytem-events', 't',
|
||||
't3datastructure', 't4', 't5', 't7', 'ta', 'tab', 'table', 'table-name',
|
||||
'tablename', 'tabpanels', 'tabs', 'tabular', 'tacacs', 'tack-down', 'tack-up',
|
||||
'taco', 'tads3', 'tag', 'tag-string', 'tag-value', 'tagbraces', 'tagdef',
|
||||
'tagged', 'tagger_script', 'taglib', 'tagname', 'tagnamedjango', 'tags',
|
||||
'taint', 'take', 'target', 'targetobj', 'targetprop', 'task', 'tasks',
|
||||
'tbdfile', 'tbl', 'tbody', 'tcl', 'tcoffee', 'tcp-object', 'td', 'tdl', 'tea',
|
||||
'team', 'telegram', 'tell', 'telnet', 'temp', 'template', 'template-call',
|
||||
'template-parameter', 'templatetag', 'tempo', 'temporal', 'term',
|
||||
'term-comparison', 'term-creation-and-decomposition', 'term-io',
|
||||
'term-testing', 'term-unification', 'terminal', 'terminate', 'termination',
|
||||
'terminator', 'terms', 'ternary', 'ternary-if', 'terra', 'terraform',
|
||||
'terrain-block', 'test', 'testcase', 'testing', 'tests', 'testsuite', 'testx',
|
||||
'tex', 'texres', 'texshop', 'text', 'text-reference', 'text-suite', 'textbf',
|
||||
'textcolor', 'textile', 'textio', 'textit', 'textlabels', 'textmate',
|
||||
'texttt', 'textual', 'texture', 'texture-map', 'texture-option', 'tfoot',
|
||||
'th', 'thead', 'then', 'therefore', 'thin', 'thing1', 'third', 'this',
|
||||
'thorn', 'thread', 'three', 'thrift', 'throughput', 'throw', 'throwables',
|
||||
'throws', 'tick', 'ticket-num', 'ticket-psa', 'tid-file', 'tidal',
|
||||
'tidalcycles', 'tiddler', 'tiddler-field', 'tiddler-fields', 'tidy', 'tier',
|
||||
'tieslur', 'tikz', 'tilde', 'time', 'timeblock', 'timehrap', 'timeout',
|
||||
'timer', 'times', 'timesig', 'timespan', 'timespec', 'timestamp', 'timing',
|
||||
'titanium', 'title', 'title-page', 'title-text', 'titled-paragraph', 'tjs',
|
||||
'tl', 'tla', 'tlh', 'tmpl', 'tmsim', 'tmux', 'tnote', 'tnsaudit', 'to',
|
||||
'to-file', 'to-type', 'toc', 'toc-list', 'todo', 'todo_extra', 'todotxt',
|
||||
'token', 'token-def', 'token-paste', 'token-type', 'tokenised', 'tokenizer',
|
||||
'toml', 'too-many-tildes', 'tool', 'toolbox', 'tooltip', 'top', 'top-level',
|
||||
'top_level', 'topas', 'topic', 'topic-decoration', 'topic-title', 'tornado',
|
||||
'torque', 'torquescript', 'tosca', 'total-config', 'totaljs', 'tpye', 'tr',
|
||||
'trace', 'trace-argument', 'trace-object', 'traceback', 'tracing',
|
||||
'track_processing', 'trader', 'tradersk', 'trail', 'trailing',
|
||||
'trailing-array-separator', 'trailing-dictionary-separator', 'trailing-match',
|
||||
'trait', 'traits', 'traits-keyword', 'transaction',
|
||||
'transcendental', 'transcludeblock', 'transcludeinline', 'transclusion',
|
||||
'transform', 'transformation', 'transient', 'transition',
|
||||
'transitionable-property-value', 'translation', 'transmission-filter',
|
||||
'transparency', 'transparent-line', 'transpose', 'transposed-func',
|
||||
'transposed-matrix', 'transposed-parens', 'transposed-variable', 'trap',
|
||||
'tree', 'treetop', 'trenni', 'trigEvent_', 'trigLevelMod_', 'trigLevel_',
|
||||
'trigger', 'trigger-words', 'triggermodifier', 'trigonometry',
|
||||
'trimming-loop', 'triple', 'triple-dash', 'triple-slash', 'triple-star',
|
||||
'true', 'truncate', 'truncation', 'truthgreen', 'try', 'try-catch',
|
||||
'trycatch', 'ts', 'tsql', 'tss', 'tst', 'tsv', 'tsx', 'tt', 'ttcn3',
|
||||
'ttlextension', 'ttpmacro', 'tts', 'tubaina', 'tubaina2', 'tul', 'tup',
|
||||
'tuple', 'turbulence', 'turing', 'turquoise', 'turtle', 'tutch', 'tvml',
|
||||
'tw5', 'twig', 'twigil', 'twiki', 'two', 'txl', 'txt', 'txt2tags', 'type',
|
||||
'type-annotation', 'type-cast', 'type-cheat', 'type-checking',
|
||||
'type-constrained', 'type-constraint', 'type-declaration', 'type-def',
|
||||
'type-definition', 'type-definition-group', 'type-definitions',
|
||||
'type-descriptor', 'type-of', 'type-or', 'type-parameter', 'type-parameters',
|
||||
'type-signature', 'type-spec', 'type-specialization', 'type-specifiers',
|
||||
'type_2', 'type_trait', 'typeabbrev', 'typeclass', 'typed', 'typed-hole',
|
||||
'typedblock', 'typedcoffeescript', 'typedecl', 'typedef', 'typeexp',
|
||||
'typehint', 'typehinted', 'typeid', 'typename', 'types', 'typesbii',
|
||||
'typescriptish', 'typographic-quotes', 'typoscript', 'typoscript2', 'u',
|
||||
'u-degree', 'u-end', 'u-offset', 'u-resolution', 'u-scale', 'u-segments',
|
||||
'u-size', 'u-start', 'u-value', 'uc', 'ucicfg', 'ucicmd', 'udaf', 'udf',
|
||||
'udl', 'udp', 'udtf', 'ui', 'ui-block', 'ui-group', 'ui-state', 'ui-subgroup',
|
||||
'uintptr', 'ujm', 'uk', 'ul', 'umbaska', 'unOp', 'unary', 'unbuffered',
|
||||
'unchecked', 'uncleared', 'unclosed', 'unclosed-string', 'unconstrained',
|
||||
'undef', 'undefined', 'underbar-circle', 'underbar-diamond', 'underbar-iota',
|
||||
'underbar-jot', 'underbar-quote', 'underbar-semicolon', 'underline',
|
||||
'underline-text', 'underlined', 'underscore', 'undocumented',
|
||||
'unescaped-quote', 'unexpected', 'unexpected-characters',
|
||||
'unexpected-extends', 'unexpected-extends-character', 'unfiled',
|
||||
'unformatted', 'unicode', 'unicode-16-bit', 'unicode-32-bit',
|
||||
'unicode-escape', 'unicode-raw', 'unicode-raw-regex', 'unified', 'unify',
|
||||
'unimplemented', 'unimportant', 'union', 'union-declaration', 'unique-id',
|
||||
'unit', 'unit-checking', 'unit-test', 'unit_test', 'unittest', 'unity',
|
||||
'unityscript', 'universal-match', 'unix', 'unknown', 'unknown-escape',
|
||||
'unknown-method', 'unknown-property-name', 'unknown-rune', 'unlabeled',
|
||||
'unless', 'unnecessary', 'unnumbered', 'uno', 'unoconfig', 'unop', 'unoproj',
|
||||
'unordered', 'unordered-block', 'unosln', 'unpack', 'unpacking', 'unparsed',
|
||||
'unqualified', 'unquoted', 'unrecognized', 'unrecognized-character',
|
||||
'unrecognized-character-escape', 'unrecognized-string-escape', 'unsafe',
|
||||
'unsigned', 'unsigned-int', 'unsized_integer', 'unsupplied', 'until',
|
||||
'untitled', 'untyped', 'unused', 'uopz', 'update', 'uppercase', 'upstream',
|
||||
'upwards', 'ur', 'uri', 'url', 'usable', 'usage', 'use', 'use-as', 'use-map',
|
||||
'use-material', 'usebean', 'usecase', 'usecase-block', 'user', 'user-defined',
|
||||
'user-defined-property', 'user-defined-type', 'user-interaction',
|
||||
'userflagsref', 'userid', 'username', 'users', 'using',
|
||||
'using-namespace-declaration', 'using_animtree', 'util', 'utilities',
|
||||
'utility', 'utxt', 'uv-resolution', 'uvu', 'uvw', 'ux', 'uxc', 'uxl', 'uz',
|
||||
'v', 'v-degree', 'v-end', 'v-offset', 'v-resolution', 'v-scale', 'v-segments',
|
||||
'v-size', 'v-start', 'v-value', 'val', 'vala', 'valgrind', 'valid',
|
||||
'valid-ampersand', 'valid-bracket', 'valign', 'value', 'value-pair',
|
||||
'value-signature', 'value-size', 'value-type', 'valuepair', 'vamos', 'vamp',
|
||||
'vane-down', 'vane-left', 'vane-right', 'vane-up', 'var',
|
||||
'var-single-variable', 'var1', 'var2', 'variable', 'variable-access',
|
||||
'variable-assignment', 'variable-declaration', 'variable-definition',
|
||||
'variable-modifier', 'variable-parameter', 'variable-reference',
|
||||
'variable-usage', 'variables', 'variabletable', 'variant',
|
||||
'variant-definition', 'varname', 'varnish', 'vars', 'vb', 'vbnet', 'vbs',
|
||||
'vc', 'vcard', 'vcd', 'vcl', 'vcs', 'vector', 'vector-load', 'vectors',
|
||||
'vehicle', 'velocity', 'vendor-prefix', 'verb', 'verbatim', 'verdict',
|
||||
'verilog', 'version', 'version-number', 'version-specification', 'vertex',
|
||||
'vertex-reference', 'vertical-blending', 'vertical-span',
|
||||
'vertical-text-cue-setting', 'vex', 'vhdl', 'vhost', 'vi', 'via',
|
||||
'video-texturing', 'video_processing', 'view', 'viewhelpers', 'vimAugroupKey',
|
||||
'vimBehaveModel', 'vimFTCmd', 'vimFTOption', 'vimFuncKey', 'vimGroupSpecial',
|
||||
'vimHiAttrib', 'vimHiClear', 'vimMapModKey', 'vimPattern', 'vimSynCase',
|
||||
'vimSynType', 'vimSyncC', 'vimSyncLinecont', 'vimSyncMatch', 'vimSyncNone',
|
||||
'vimSyncRegion', 'vimUserAttrbCmplt', 'vimUserAttrbKey', 'vimUserCommand',
|
||||
'viml', 'virtual', 'virtual-host', 'virtual-reality', 'visibility',
|
||||
'visualforce', 'visualization', 'vlanhdr', 'vle', 'vmap', 'vmx', 'voice',
|
||||
'void', 'volatile', 'volt', 'volume', 'vpath', 'vplus', 'vrf', 'vtt', 'vue',
|
||||
'vue-jade', 'vue-stylus', 'w-offset', 'w-scale', 'w-value',
|
||||
'w3c-extended-color-name', 'w3c-non-standard-color-name',
|
||||
'w3c-standard-color-name', 'wait', 'waitress', 'waitress-config',
|
||||
'waitress-rb', 'warn', 'warning', 'warnings', 'wast', 'water', 'watson-todo',
|
||||
'wavefront', 'wavelet', 'wddx', 'wdiff', 'weapon', 'weave', 'weaveBracket',
|
||||
'weaveBullet', 'webidl', 'webspeed', 'webvtt', 'weekday', 'weirdland', 'wf',
|
||||
'wh', 'whatever', 'wheeled-vehicle', 'when', 'where', 'while',
|
||||
'while-condition', 'while-loop', 'whiskey', 'white', 'whitespace', 'widget',
|
||||
'width', 'wiki', 'wiki-link', 'wildcard', 'wildsk', 'win', 'window',
|
||||
'window-classes', 'windows', 'winered', 'with', 'with-arg', 'with-args',
|
||||
'with-arguments', 'with-params', 'with-prefix', 'with-side-effects',
|
||||
'with-suffix', 'with-terminator', 'with-value', 'with_colon', 'without-args',
|
||||
'without-arguments', 'wla-dx', 'word', 'word-op', 'wordnet', 'wordpress',
|
||||
'words', 'workitem', 'world', 'wow', 'wp', 'write', 'wrong',
|
||||
'wrong-access-type', 'wrong-division', 'wrong-division-assignment', 'ws',
|
||||
'www', 'wxml', 'wysiwyg-string', 'x10', 'x86', 'x86_64', 'x86asm', 'xacro',
|
||||
'xbase', 'xchg', 'xhp', 'xhprof', 'xikij', 'xml', 'xml-attr', 'xmlrpc',
|
||||
'xmlwriter', 'xop', 'xor', 'xparse', 'xq', 'xquery', 'xref', 'xsave',
|
||||
'xsd-all', 'xsd_nillable', 'xsd_optional', 'xsl', 'xslt', 'xsse3_simd', 'xst',
|
||||
'xtend', 'xtoy', 'xtpl', 'xu', 'xvc', 'xve', 'xyzw', 'y', 'y1', 'y2', 'yabb',
|
||||
'yaml', 'yaml-ext', 'yang', 'yara', 'yate', 'yaws', 'year', 'yellow', 'yield',
|
||||
'ykk', 'yorick', 'you-forgot-semicolon', 'z', 'z80', 'zap', 'zapper', 'zep',
|
||||
'zepon', 'zepto', 'zero', 'zero-width-marker', 'zero-width-print', 'zeroop',
|
||||
'zh-CN', 'zh-TW', 'zig', 'zilde', 'zlib', 'zoomfilter', 'zzz'
|
||||
])
|
||||
@@ -1,68 +0,0 @@
|
||||
{Disposable} = require 'event-kit'
|
||||
|
||||
# Extended: Manages the deserializers used for serialized state
|
||||
#
|
||||
# An instance of this class is always available as the `atom.deserializers`
|
||||
# global.
|
||||
#
|
||||
# ## Examples
|
||||
#
|
||||
# ```coffee
|
||||
# class MyPackageView extends View
|
||||
# atom.deserializers.add(this)
|
||||
#
|
||||
# @deserialize: (state) ->
|
||||
# new MyPackageView(state)
|
||||
#
|
||||
# constructor: (@state) ->
|
||||
#
|
||||
# serialize: ->
|
||||
# @state
|
||||
# ```
|
||||
module.exports =
|
||||
class DeserializerManager
|
||||
constructor: (@atomEnvironment) ->
|
||||
@deserializers = {}
|
||||
|
||||
# Public: Register the given class(es) as deserializers.
|
||||
#
|
||||
# * `deserializers` One or more deserializers to register. A deserializer can
|
||||
# be any object with a `.name` property and a `.deserialize()` method. A
|
||||
# common approach is to register a *constructor* as the deserializer for its
|
||||
# instances by adding a `.deserialize()` class method. When your method is
|
||||
# called, it will be passed serialized state as the first argument and the
|
||||
# {Atom} environment object as the second argument, which is useful if you
|
||||
# wish to avoid referencing the `atom` global.
|
||||
add: (deserializers...) ->
|
||||
@deserializers[deserializer.name] = deserializer for deserializer in deserializers
|
||||
new Disposable =>
|
||||
delete @deserializers[deserializer.name] for deserializer in deserializers
|
||||
return
|
||||
|
||||
getDeserializerCount: ->
|
||||
Object.keys(@deserializers).length
|
||||
|
||||
# Public: Deserialize the state and params.
|
||||
#
|
||||
# * `state` The state {Object} to deserialize.
|
||||
deserialize: (state) ->
|
||||
return unless state?
|
||||
|
||||
if deserializer = @get(state)
|
||||
stateVersion = state.get?('version') ? state.version
|
||||
return if deserializer.version? and deserializer.version isnt stateVersion
|
||||
deserializer.deserialize(state, @atomEnvironment)
|
||||
else
|
||||
console.warn "No deserializer found for", state
|
||||
|
||||
# Get the deserializer for the state.
|
||||
#
|
||||
# * `state` The state {Object} being deserialized.
|
||||
get: (state) ->
|
||||
return unless state?
|
||||
|
||||
name = state.get?('deserializer') ? state.deserializer
|
||||
@deserializers[name]
|
||||
|
||||
clear: ->
|
||||
@deserializers = {}
|
||||
99
src/deserializer-manager.js
Normal file
99
src/deserializer-manager.js
Normal file
@@ -0,0 +1,99 @@
|
||||
const {Disposable} = require('event-kit')
|
||||
|
||||
// Extended: Manages the deserializers used for serialized state
|
||||
//
|
||||
// An instance of this class is always available as the `atom.deserializers`
|
||||
// global.
|
||||
//
|
||||
// ## Examples
|
||||
//
|
||||
// ```coffee
|
||||
// class MyPackageView extends View
|
||||
// atom.deserializers.add(this)
|
||||
//
|
||||
// @deserialize: (state) ->
|
||||
// new MyPackageView(state)
|
||||
//
|
||||
// constructor: (@state) ->
|
||||
//
|
||||
// serialize: ->
|
||||
// @state
|
||||
// ```
|
||||
module.exports =
|
||||
class DeserializerManager {
|
||||
constructor (atomEnvironment) {
|
||||
this.atomEnvironment = atomEnvironment
|
||||
this.deserializers = {}
|
||||
}
|
||||
|
||||
// Public: Register the given class(es) as deserializers.
|
||||
//
|
||||
// * `deserializers` One or more deserializers to register. A deserializer can
|
||||
// be any object with a `.name` property and a `.deserialize()` method. A
|
||||
// common approach is to register a *constructor* as the deserializer for its
|
||||
// instances by adding a `.deserialize()` class method. When your method is
|
||||
// called, it will be passed serialized state as the first argument and the
|
||||
// {AtomEnvironment} object as the second argument, which is useful if you
|
||||
// wish to avoid referencing the `atom` global.
|
||||
add (...deserializers) {
|
||||
for (let i = 0; i < deserializers.length; i++) {
|
||||
let deserializer = deserializers[i]
|
||||
this.deserializers[deserializer.name] = deserializer
|
||||
}
|
||||
|
||||
return new Disposable(() => {
|
||||
for (let j = 0; j < deserializers.length; j++) {
|
||||
let deserializer = deserializers[j]
|
||||
delete this.deserializers[deserializer.name]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getDeserializerCount () {
|
||||
return Object.keys(this.deserializers).length
|
||||
}
|
||||
|
||||
// Public: Deserialize the state and params.
|
||||
//
|
||||
// * `state` The state {Object} to deserialize.
|
||||
deserialize (state) {
|
||||
if (state == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const deserializer = this.get(state)
|
||||
if (deserializer) {
|
||||
let stateVersion = (
|
||||
(typeof state.get === 'function') && state.get('version') ||
|
||||
state.version
|
||||
)
|
||||
|
||||
if ((deserializer.version != null) && deserializer.version !== stateVersion) {
|
||||
return
|
||||
}
|
||||
return deserializer.deserialize(state, this.atomEnvironment)
|
||||
} else {
|
||||
return console.warn('No deserializer found for', state)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the deserializer for the state.
|
||||
//
|
||||
// * `state` The state {Object} being deserialized.
|
||||
get (state) {
|
||||
if (state == null) {
|
||||
return
|
||||
}
|
||||
|
||||
let stateDeserializer = (
|
||||
(typeof state.get === 'function') && state.get('deserializer') ||
|
||||
state.deserializer
|
||||
)
|
||||
|
||||
return this.deserializers[stateDeserializer]
|
||||
}
|
||||
|
||||
clear () {
|
||||
this.deserializers = {}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
861
src/dock.js
Normal file
861
src/dock.js
Normal file
@@ -0,0 +1,861 @@
|
||||
const etch = require('etch')
|
||||
const _ = require('underscore-plus')
|
||||
const {CompositeDisposable, Emitter} = require('event-kit')
|
||||
const PaneContainer = require('./pane-container')
|
||||
const TextEditor = require('./text-editor')
|
||||
const Grim = require('grim')
|
||||
|
||||
const $ = etch.dom
|
||||
const MINIMUM_SIZE = 100
|
||||
const DEFAULT_INITIAL_SIZE = 300
|
||||
const SHOULD_ANIMATE_CLASS = 'atom-dock-should-animate'
|
||||
const VISIBLE_CLASS = 'atom-dock-open'
|
||||
const RESIZE_HANDLE_RESIZABLE_CLASS = 'atom-dock-resize-handle-resizable'
|
||||
const TOGGLE_BUTTON_VISIBLE_CLASS = 'atom-dock-toggle-button-visible'
|
||||
const CURSOR_OVERLAY_VISIBLE_CLASS = 'atom-dock-cursor-overlay-visible'
|
||||
|
||||
// Extended: A container at the edges of the editor window capable of holding items.
|
||||
// You should not create a Dock directly. Instead, access one of the three docks of the workspace
|
||||
// via {Workspace::getLeftDock}, {Workspace::getRightDock}, and {Workspace::getBottomDock}
|
||||
// or add an item to a dock via {Workspace::open}.
|
||||
module.exports = class Dock {
|
||||
constructor (params) {
|
||||
this.handleResizeHandleDragStart = this.handleResizeHandleDragStart.bind(this)
|
||||
this.handleResizeToFit = this.handleResizeToFit.bind(this)
|
||||
this.handleMouseMove = this.handleMouseMove.bind(this)
|
||||
this.handleMouseUp = this.handleMouseUp.bind(this)
|
||||
this.handleDrag = _.throttle(this.handleDrag.bind(this), 30)
|
||||
this.handleDragEnd = this.handleDragEnd.bind(this)
|
||||
this.handleToggleButtonDragEnter = this.handleToggleButtonDragEnter.bind(this)
|
||||
this.toggle = this.toggle.bind(this)
|
||||
|
||||
this.location = params.location
|
||||
this.widthOrHeight = getWidthOrHeight(this.location)
|
||||
this.config = params.config
|
||||
this.applicationDelegate = params.applicationDelegate
|
||||
this.deserializerManager = params.deserializerManager
|
||||
this.notificationManager = params.notificationManager
|
||||
this.viewRegistry = params.viewRegistry
|
||||
this.didActivate = params.didActivate
|
||||
|
||||
this.emitter = new Emitter()
|
||||
|
||||
this.paneContainer = new PaneContainer({
|
||||
location: this.location,
|
||||
config: this.config,
|
||||
applicationDelegate: this.applicationDelegate,
|
||||
deserializerManager: this.deserializerManager,
|
||||
notificationManager: this.notificationManager,
|
||||
viewRegistry: this.viewRegistry
|
||||
})
|
||||
|
||||
this.state = {
|
||||
size: null,
|
||||
visible: false,
|
||||
shouldAnimate: false
|
||||
}
|
||||
|
||||
this.subscriptions = new CompositeDisposable(
|
||||
this.emitter,
|
||||
this.paneContainer.onDidActivatePane(() => {
|
||||
this.show()
|
||||
this.didActivate(this)
|
||||
}),
|
||||
this.paneContainer.observePanes(pane => {
|
||||
pane.onDidAddItem(this.handleDidAddPaneItem.bind(this))
|
||||
pane.onDidRemoveItem(this.handleDidRemovePaneItem.bind(this))
|
||||
}),
|
||||
this.paneContainer.onDidChangeActivePane((item) => params.didChangeActivePane(this, item)),
|
||||
this.paneContainer.onDidChangeActivePaneItem((item) => params.didChangeActivePaneItem(this, item)),
|
||||
this.paneContainer.onDidDestroyPaneItem((item) => params.didDestroyPaneItem(item))
|
||||
)
|
||||
}
|
||||
|
||||
// This method is called explicitly by the object which adds the Dock to the document.
|
||||
elementAttached () {
|
||||
// Re-render when the dock is attached to make sure we remeasure sizes defined in CSS.
|
||||
etch.updateSync(this)
|
||||
}
|
||||
|
||||
getElement () {
|
||||
// Because this code is included in the snapshot, we have to make sure we don't touch the DOM
|
||||
// during initialization. Therefore, we defer initialization of the component (which creates a
|
||||
// DOM element) until somebody asks for the element.
|
||||
if (this.element == null) {
|
||||
etch.initialize(this)
|
||||
}
|
||||
return this.element
|
||||
}
|
||||
|
||||
getLocation () {
|
||||
return this.location
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.subscriptions.dispose()
|
||||
this.paneContainer.destroy()
|
||||
window.removeEventListener('mousemove', this.handleMouseMove)
|
||||
window.removeEventListener('mouseup', this.handleMouseUp)
|
||||
window.removeEventListener('drag', this.handleDrag)
|
||||
window.removeEventListener('dragend', this.handleDragEnd)
|
||||
}
|
||||
|
||||
setHovered (hovered) {
|
||||
if (hovered === this.state.hovered) return
|
||||
this.setState({hovered})
|
||||
}
|
||||
|
||||
setDraggingItem (draggingItem) {
|
||||
if (draggingItem === this.state.draggingItem) return
|
||||
this.setState({draggingItem})
|
||||
}
|
||||
|
||||
// Extended: Show the dock and focus its active {Pane}.
|
||||
activate () {
|
||||
this.getActivePane().activate()
|
||||
}
|
||||
|
||||
// Extended: Show the dock without focusing it.
|
||||
show () {
|
||||
this.setState({visible: true})
|
||||
}
|
||||
|
||||
// Extended: Hide the dock and activate the {WorkspaceCenter} if the dock was
|
||||
// was previously focused.
|
||||
hide () {
|
||||
this.setState({visible: false})
|
||||
}
|
||||
|
||||
// Extended: Toggle the dock's visibility without changing the {Workspace}'s
|
||||
// active pane container.
|
||||
toggle () {
|
||||
const state = {visible: !this.state.visible}
|
||||
if (!state.visible) state.hovered = false
|
||||
this.setState(state)
|
||||
}
|
||||
|
||||
// Extended: Check if the dock is visible.
|
||||
//
|
||||
// Returns a {Boolean}.
|
||||
isVisible () {
|
||||
return this.state.visible
|
||||
}
|
||||
|
||||
setState (newState) {
|
||||
const prevState = this.state
|
||||
const nextState = Object.assign({}, prevState, newState)
|
||||
|
||||
// Update the `shouldAnimate` state. This needs to be written to the DOM before updating the
|
||||
// class that changes the animated property. Normally we'd have to defer the class change a
|
||||
// frame to ensure the property is animated (or not) appropriately, however we luck out in this
|
||||
// case because the drag start always happens before the item is dragged into the toggle button.
|
||||
if (nextState.visible !== prevState.visible) {
|
||||
// Never animate toggling visibility...
|
||||
nextState.shouldAnimate = false
|
||||
} else if (!nextState.visible && nextState.draggingItem && !prevState.draggingItem) {
|
||||
// ...but do animate if you start dragging while the panel is hidden.
|
||||
nextState.shouldAnimate = true
|
||||
}
|
||||
|
||||
this.state = nextState
|
||||
|
||||
const {hovered, visible} = this.state
|
||||
|
||||
// Render immediately if the dock becomes visible or the size changes in case people are
|
||||
// measuring after opening, for example.
|
||||
if (this.element != null) {
|
||||
if ((visible && !prevState.visible) || (this.state.size !== prevState.size)) etch.updateSync(this)
|
||||
else etch.update(this)
|
||||
}
|
||||
|
||||
if (hovered !== prevState.hovered) {
|
||||
this.emitter.emit('did-change-hovered', hovered)
|
||||
}
|
||||
if (visible !== prevState.visible) {
|
||||
this.emitter.emit('did-change-visible', visible)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const innerElementClassList = ['atom-dock-inner', this.location]
|
||||
if (this.state.visible) innerElementClassList.push(VISIBLE_CLASS)
|
||||
|
||||
const maskElementClassList = ['atom-dock-mask']
|
||||
if (this.state.shouldAnimate) maskElementClassList.push(SHOULD_ANIMATE_CLASS)
|
||||
|
||||
const cursorOverlayElementClassList = ['atom-dock-cursor-overlay', this.location]
|
||||
if (this.state.resizing) cursorOverlayElementClassList.push(CURSOR_OVERLAY_VISIBLE_CLASS)
|
||||
|
||||
const shouldBeVisible = this.state.visible || this.state.showDropTarget
|
||||
const size = Math.max(MINIMUM_SIZE,
|
||||
this.state.size ||
|
||||
(this.state.draggingItem && getPreferredSize(this.state.draggingItem, this.location)) ||
|
||||
DEFAULT_INITIAL_SIZE
|
||||
)
|
||||
|
||||
// We need to change the size of the mask...
|
||||
const maskStyle = {[this.widthOrHeight]: `${shouldBeVisible ? size : 0}px`}
|
||||
// ...but the content needs to maintain a constant size.
|
||||
const wrapperStyle = {[this.widthOrHeight]: `${size}px`}
|
||||
|
||||
return $(
|
||||
'atom-dock',
|
||||
{className: this.location},
|
||||
$.div(
|
||||
{ref: 'innerElement', className: innerElementClassList.join(' ')},
|
||||
$.div(
|
||||
{
|
||||
className: maskElementClassList.join(' '),
|
||||
style: maskStyle
|
||||
},
|
||||
$.div(
|
||||
{
|
||||
ref: 'wrapperElement',
|
||||
className: `atom-dock-content-wrapper ${this.location}`,
|
||||
style: wrapperStyle
|
||||
},
|
||||
$(DockResizeHandle, {
|
||||
location: this.location,
|
||||
onResizeStart: this.handleResizeHandleDragStart,
|
||||
onResizeToFit: this.handleResizeToFit,
|
||||
dockIsVisible: this.state.visible
|
||||
}),
|
||||
$(ElementComponent, {element: this.paneContainer.getElement()}),
|
||||
$.div({className: cursorOverlayElementClassList.join(' ')})
|
||||
)
|
||||
),
|
||||
$(DockToggleButton, {
|
||||
ref: 'toggleButton',
|
||||
onDragEnter: this.state.draggingItem ? this.handleToggleButtonDragEnter : null,
|
||||
location: this.location,
|
||||
toggle: this.toggle,
|
||||
dockIsVisible: shouldBeVisible,
|
||||
visible:
|
||||
// Don't show the toggle button if the dock is closed and empty...
|
||||
(this.state.hovered &&
|
||||
(this.state.visible || this.getPaneItems().length > 0)) ||
|
||||
// ...or if the item can't be dropped in that dock.
|
||||
(!shouldBeVisible &&
|
||||
this.state.draggingItem &&
|
||||
isItemAllowed(this.state.draggingItem, this.location))
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
update (props) {
|
||||
// Since we're interopping with non-etch stuff, this method's actually never called.
|
||||
return etch.update(this)
|
||||
}
|
||||
|
||||
handleDidAddPaneItem () {
|
||||
if (this.state.size == null) {
|
||||
this.setState({size: this.getInitialSize()})
|
||||
}
|
||||
}
|
||||
|
||||
handleDidRemovePaneItem () {
|
||||
// Hide the dock if you remove the last item.
|
||||
if (this.paneContainer.getPaneItems().length === 0) {
|
||||
this.setState({visible: false, hovered: false, size: null})
|
||||
}
|
||||
}
|
||||
|
||||
handleResizeHandleDragStart () {
|
||||
window.addEventListener('mousemove', this.handleMouseMove)
|
||||
window.addEventListener('mouseup', this.handleMouseUp)
|
||||
this.setState({resizing: true})
|
||||
}
|
||||
|
||||
handleResizeToFit () {
|
||||
const item = this.getActivePaneItem()
|
||||
if (item) {
|
||||
const size = getPreferredSize(item, this.getLocation())
|
||||
if (size != null) this.setState({size})
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseMove (event) {
|
||||
if (event.buttons === 0) { // We missed the mouseup event. For some reason it happens on Windows
|
||||
this.handleMouseUp(event)
|
||||
return
|
||||
}
|
||||
|
||||
let size = 0
|
||||
switch (this.location) {
|
||||
case 'left':
|
||||
size = event.pageX - this.element.getBoundingClientRect().left
|
||||
break
|
||||
case 'bottom':
|
||||
size = this.element.getBoundingClientRect().bottom - event.pageY
|
||||
break
|
||||
case 'right':
|
||||
size = this.element.getBoundingClientRect().right - event.pageX
|
||||
break
|
||||
}
|
||||
this.setState({size})
|
||||
}
|
||||
|
||||
handleMouseUp (event) {
|
||||
window.removeEventListener('mousemove', this.handleMouseMove)
|
||||
window.removeEventListener('mouseup', this.handleMouseUp)
|
||||
this.setState({resizing: false})
|
||||
}
|
||||
|
||||
handleToggleButtonDragEnter () {
|
||||
this.setState({showDropTarget: true})
|
||||
window.addEventListener('drag', this.handleDrag)
|
||||
window.addEventListener('dragend', this.handleDragEnd)
|
||||
}
|
||||
|
||||
handleDrag (event) {
|
||||
if (!this.pointWithinHoverArea({x: event.pageX, y: event.pageY}, true)) {
|
||||
this.draggedOut()
|
||||
}
|
||||
}
|
||||
|
||||
handleDragEnd () {
|
||||
this.draggedOut()
|
||||
}
|
||||
|
||||
draggedOut () {
|
||||
this.setState({showDropTarget: false})
|
||||
window.removeEventListener('drag', this.handleDrag)
|
||||
window.removeEventListener('dragend', this.handleDragEnd)
|
||||
}
|
||||
|
||||
// Determine whether the cursor is within the dock hover area. This isn't as simple as just using
|
||||
// mouseenter/leave because we want to be a little more forgiving. For example, if the cursor is
|
||||
// over the footer, we want to show the bottom dock's toggle button. Also note that our criteria
|
||||
// for detecting entry are different than detecting exit but, in order for us to avoid jitter, the
|
||||
// area considered when detecting exit MUST fully encompass the area considered when detecting
|
||||
// entry.
|
||||
pointWithinHoverArea (point, detectingExit) {
|
||||
const dockBounds = this.refs.innerElement.getBoundingClientRect()
|
||||
|
||||
// Copy the bounds object since we can't mutate it.
|
||||
const bounds = {
|
||||
top: dockBounds.top,
|
||||
right: dockBounds.right,
|
||||
bottom: dockBounds.bottom,
|
||||
left: dockBounds.left
|
||||
}
|
||||
|
||||
// To provide a minimum target, expand the area toward the center a bit.
|
||||
switch (this.location) {
|
||||
case 'right':
|
||||
bounds.left = Math.min(bounds.left, bounds.right - 2)
|
||||
break
|
||||
case 'bottom':
|
||||
bounds.top = Math.min(bounds.top, bounds.bottom - 1)
|
||||
break
|
||||
case 'left':
|
||||
bounds.right = Math.max(bounds.right, bounds.left + 2)
|
||||
break
|
||||
}
|
||||
|
||||
// Further expand the area to include all panels that are closer to the edge than the dock.
|
||||
switch (this.location) {
|
||||
case 'right':
|
||||
bounds.right = Number.POSITIVE_INFINITY
|
||||
break
|
||||
case 'bottom':
|
||||
bounds.bottom = Number.POSITIVE_INFINITY
|
||||
break
|
||||
case 'left':
|
||||
bounds.left = Number.NEGATIVE_INFINITY
|
||||
break
|
||||
}
|
||||
|
||||
// If we're in this area, we know we're within the hover area without having to take further
|
||||
// measurements.
|
||||
if (rectContainsPoint(bounds, point)) return true
|
||||
|
||||
// If we're within the toggle button, we're definitely in the hover area. Unfortunately, we
|
||||
// can't do this measurement conditionally (e.g. only if the toggle button is visible) because
|
||||
// our knowledge of the toggle's button is incomplete due to CSS animations. (We may think the
|
||||
// toggle button isn't visible when in actuality it is, but is animating to its hidden state.)
|
||||
//
|
||||
// Since `point` is always the current mouse position, one possible optimization would be to
|
||||
// remove it as an argument and determine whether we're inside the toggle button using
|
||||
// mouseenter/leave events on it. This class would still need to keep track of the mouse
|
||||
// position (via a mousemove listener) for the other measurements, though.
|
||||
const toggleButtonBounds = this.refs.toggleButton.getBounds()
|
||||
if (rectContainsPoint(toggleButtonBounds, point)) return true
|
||||
|
||||
// The area used when detecting exit is actually larger than when detecting entrances. Expand
|
||||
// our bounds and recheck them.
|
||||
if (detectingExit) {
|
||||
const hoverMargin = 20
|
||||
switch (this.location) {
|
||||
case 'right':
|
||||
bounds.left = Math.min(bounds.left, toggleButtonBounds.left) - hoverMargin
|
||||
break
|
||||
case 'bottom':
|
||||
bounds.top = Math.min(bounds.top, toggleButtonBounds.top) - hoverMargin
|
||||
break
|
||||
case 'left':
|
||||
bounds.right = Math.max(bounds.right, toggleButtonBounds.right) + hoverMargin
|
||||
break
|
||||
}
|
||||
if (rectContainsPoint(bounds, point)) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
getInitialSize () {
|
||||
// The item may not have been activated yet. If that's the case, just use the first item.
|
||||
const activePaneItem = this.paneContainer.getActivePaneItem() || this.paneContainer.getPaneItems()[0]
|
||||
// If there are items, we should have an explicit width; if not, we shouldn't.
|
||||
return activePaneItem
|
||||
? getPreferredSize(activePaneItem, this.location) || DEFAULT_INITIAL_SIZE
|
||||
: null
|
||||
}
|
||||
|
||||
serialize () {
|
||||
return {
|
||||
deserializer: 'Dock',
|
||||
size: this.state.size,
|
||||
paneContainer: this.paneContainer.serialize(),
|
||||
visible: this.state.visible
|
||||
}
|
||||
}
|
||||
|
||||
deserialize (serialized, deserializerManager) {
|
||||
this.paneContainer.deserialize(serialized.paneContainer, deserializerManager)
|
||||
this.setState({
|
||||
size: serialized.size || this.getInitialSize(),
|
||||
// If no items could be deserialized, we don't want to show the dock (even if it was visible last time)
|
||||
visible: serialized.visible && (this.paneContainer.getPaneItems().length > 0)
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Event Subscription
|
||||
*/
|
||||
|
||||
// Essential: Invoke the given callback when the visibility of the dock changes.
|
||||
//
|
||||
// * `callback` {Function} to be called when the visibility changes.
|
||||
// * `visible` {Boolean} Is the dock now visible?
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeVisible (callback) {
|
||||
return this.emitter.on('did-change-visible', callback)
|
||||
}
|
||||
|
||||
// Essential: Invoke the given callback with the current and all future visibilities of the dock.
|
||||
//
|
||||
// * `callback` {Function} to be called when the visibility changes.
|
||||
// * `visible` {Boolean} Is the dock now visible?
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
observeVisible (callback) {
|
||||
callback(this.isVisible())
|
||||
return this.onDidChangeVisible(callback)
|
||||
}
|
||||
|
||||
// Essential: Invoke the given callback with all current and future panes items
|
||||
// in the dock.
|
||||
//
|
||||
// * `callback` {Function} to be called with current and future pane items.
|
||||
// * `item` An item that is present in {::getPaneItems} at the time of
|
||||
// subscription or that is added at some later time.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
observePaneItems (callback) {
|
||||
return this.paneContainer.observePaneItems(callback)
|
||||
}
|
||||
|
||||
// Essential: Invoke the given callback when the active pane item changes.
|
||||
//
|
||||
// Because observers are invoked synchronously, it's important not to perform
|
||||
// any expensive operations via this method. Consider
|
||||
// {::onDidStopChangingActivePaneItem} to delay operations until after changes
|
||||
// stop occurring.
|
||||
//
|
||||
// * `callback` {Function} to be called when the active pane item changes.
|
||||
// * `item` The active pane item.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeActivePaneItem (callback) {
|
||||
return this.paneContainer.onDidChangeActivePaneItem(callback)
|
||||
}
|
||||
|
||||
// Essential: Invoke the given callback when the active pane item stops
|
||||
// changing.
|
||||
//
|
||||
// Observers are called asynchronously 100ms after the last active pane item
|
||||
// change. Handling changes here rather than in the synchronous
|
||||
// {::onDidChangeActivePaneItem} prevents unneeded work if the user is quickly
|
||||
// changing or closing tabs and ensures critical UI feedback, like changing the
|
||||
// highlighted tab, gets priority over work that can be done asynchronously.
|
||||
//
|
||||
// * `callback` {Function} to be called when the active pane item stopts
|
||||
// changing.
|
||||
// * `item` The active pane item.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidStopChangingActivePaneItem (callback) {
|
||||
return this.paneContainer.onDidStopChangingActivePaneItem(callback)
|
||||
}
|
||||
|
||||
// Essential: Invoke the given callback with the current active pane item and
|
||||
// with all future active pane items in the dock.
|
||||
//
|
||||
// * `callback` {Function} to be called when the active pane item changes.
|
||||
// * `item` The current active pane item.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
observeActivePaneItem (callback) {
|
||||
return this.paneContainer.observeActivePaneItem(callback)
|
||||
}
|
||||
|
||||
// Extended: Invoke the given callback when a pane is added to the dock.
|
||||
//
|
||||
// * `callback` {Function} to be called panes are added.
|
||||
// * `event` {Object} with the following keys:
|
||||
// * `pane` The added pane.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidAddPane (callback) {
|
||||
return this.paneContainer.onDidAddPane(callback)
|
||||
}
|
||||
|
||||
// Extended: Invoke the given callback before a pane is destroyed in the
|
||||
// dock.
|
||||
//
|
||||
// * `callback` {Function} to be called before panes are destroyed.
|
||||
// * `event` {Object} with the following keys:
|
||||
// * `pane` The pane to be destroyed.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onWillDestroyPane (callback) {
|
||||
return this.paneContainer.onWillDestroyPane(callback)
|
||||
}
|
||||
|
||||
// Extended: Invoke the given callback when a pane is destroyed in the dock.
|
||||
//
|
||||
// * `callback` {Function} to be called panes are destroyed.
|
||||
// * `event` {Object} with the following keys:
|
||||
// * `pane` The destroyed pane.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroyPane (callback) {
|
||||
return this.paneContainer.onDidDestroyPane(callback)
|
||||
}
|
||||
|
||||
// Extended: Invoke the given callback with all current and future panes in the
|
||||
// dock.
|
||||
//
|
||||
// * `callback` {Function} to be called with current and future panes.
|
||||
// * `pane` A {Pane} that is present in {::getPanes} at the time of
|
||||
// subscription or that is added at some later time.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
observePanes (callback) {
|
||||
return this.paneContainer.observePanes(callback)
|
||||
}
|
||||
|
||||
// Extended: Invoke the given callback when the active pane changes.
|
||||
//
|
||||
// * `callback` {Function} to be called when the active pane changes.
|
||||
// * `pane` A {Pane} that is the current return value of {::getActivePane}.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeActivePane (callback) {
|
||||
return this.paneContainer.onDidChangeActivePane(callback)
|
||||
}
|
||||
|
||||
// Extended: Invoke the given callback with the current active pane and when
|
||||
// the active pane changes.
|
||||
//
|
||||
// * `callback` {Function} to be called with the current and future active#
|
||||
// panes.
|
||||
// * `pane` A {Pane} that is the current return value of {::getActivePane}.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
observeActivePane (callback) {
|
||||
return this.paneContainer.observeActivePane(callback)
|
||||
}
|
||||
|
||||
// Extended: Invoke the given callback when a pane item is added to the dock.
|
||||
//
|
||||
// * `callback` {Function} to be called when pane items are added.
|
||||
// * `event` {Object} with the following keys:
|
||||
// * `item` The added pane item.
|
||||
// * `pane` {Pane} containing the added item.
|
||||
// * `index` {Number} indicating the index of the added item in its pane.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidAddPaneItem (callback) {
|
||||
return this.paneContainer.onDidAddPaneItem(callback)
|
||||
}
|
||||
|
||||
// Extended: Invoke the given callback when a pane item is about to be
|
||||
// destroyed, before the user is prompted to save it.
|
||||
//
|
||||
// * `callback` {Function} to be called before pane items are destroyed.
|
||||
// * `event` {Object} with the following keys:
|
||||
// * `item` The item to be destroyed.
|
||||
// * `pane` {Pane} containing the item to be destroyed.
|
||||
// * `index` {Number} indicating the index of the item to be destroyed in
|
||||
// its pane.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose` can be called to unsubscribe.
|
||||
onWillDestroyPaneItem (callback) {
|
||||
return this.paneContainer.onWillDestroyPaneItem(callback)
|
||||
}
|
||||
|
||||
// Extended: Invoke the given callback when a pane item is destroyed.
|
||||
//
|
||||
// * `callback` {Function} to be called when pane items are destroyed.
|
||||
// * `event` {Object} with the following keys:
|
||||
// * `item` The destroyed item.
|
||||
// * `pane` {Pane} containing the destroyed item.
|
||||
// * `index` {Number} indicating the index of the destroyed item in its
|
||||
// pane.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose` can be called to unsubscribe.
|
||||
onDidDestroyPaneItem (callback) {
|
||||
return this.paneContainer.onDidDestroyPaneItem(callback)
|
||||
}
|
||||
|
||||
// Extended: Invoke the given callback when the hovered state of the dock changes.
|
||||
//
|
||||
// * `callback` {Function} to be called when the hovered state changes.
|
||||
// * `hovered` {Boolean} Is the dock now hovered?
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeHovered (callback) {
|
||||
return this.emitter.on('did-change-hovered', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Pane Items
|
||||
*/
|
||||
|
||||
// Essential: Get all pane items in the dock.
|
||||
//
|
||||
// Returns an {Array} of items.
|
||||
getPaneItems () {
|
||||
return this.paneContainer.getPaneItems()
|
||||
}
|
||||
|
||||
// Essential: Get the active {Pane}'s active item.
|
||||
//
|
||||
// Returns an pane item {Object}.
|
||||
getActivePaneItem () {
|
||||
return this.paneContainer.getActivePaneItem()
|
||||
}
|
||||
|
||||
// Deprecated: Get the active item if it is a {TextEditor}.
|
||||
//
|
||||
// Returns a {TextEditor} or `undefined` if the current active item is not a
|
||||
// {TextEditor}.
|
||||
getActiveTextEditor () {
|
||||
Grim.deprecate('Text editors are not allowed in docks. Use atom.workspace.getActiveTextEditor() instead.')
|
||||
|
||||
const activeItem = this.getActivePaneItem()
|
||||
if (activeItem instanceof TextEditor) { return activeItem }
|
||||
}
|
||||
|
||||
// Save all pane items.
|
||||
saveAll () {
|
||||
this.paneContainer.saveAll()
|
||||
}
|
||||
|
||||
confirmClose (options) {
|
||||
return this.paneContainer.confirmClose(options)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Panes
|
||||
*/
|
||||
|
||||
// Extended: Get all panes in the dock.
|
||||
//
|
||||
// Returns an {Array} of {Pane}s.
|
||||
getPanes () {
|
||||
return this.paneContainer.getPanes()
|
||||
}
|
||||
|
||||
// Extended: Get the active {Pane}.
|
||||
//
|
||||
// Returns a {Pane}.
|
||||
getActivePane () {
|
||||
return this.paneContainer.getActivePane()
|
||||
}
|
||||
|
||||
// Extended: Make the next pane active.
|
||||
activateNextPane () {
|
||||
return this.paneContainer.activateNextPane()
|
||||
}
|
||||
|
||||
// Extended: Make the previous pane active.
|
||||
activatePreviousPane () {
|
||||
return this.paneContainer.activatePreviousPane()
|
||||
}
|
||||
|
||||
paneForURI (uri) {
|
||||
return this.paneContainer.paneForURI(uri)
|
||||
}
|
||||
|
||||
paneForItem (item) {
|
||||
return this.paneContainer.paneForItem(item)
|
||||
}
|
||||
|
||||
// Destroy (close) the active pane.
|
||||
destroyActivePane () {
|
||||
const activePane = this.getActivePane()
|
||||
if (activePane != null) {
|
||||
activePane.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DockResizeHandle {
|
||||
constructor (props) {
|
||||
this.props = props
|
||||
etch.initialize(this)
|
||||
}
|
||||
|
||||
render () {
|
||||
const classList = ['atom-dock-resize-handle', this.props.location]
|
||||
if (this.props.dockIsVisible) classList.push(RESIZE_HANDLE_RESIZABLE_CLASS)
|
||||
|
||||
return $.div({
|
||||
className: classList.join(' '),
|
||||
on: {mousedown: this.handleMouseDown}
|
||||
})
|
||||
}
|
||||
|
||||
getElement () {
|
||||
return this.element
|
||||
}
|
||||
|
||||
getSize () {
|
||||
if (!this.size) {
|
||||
this.size = this.element.getBoundingClientRect()[getWidthOrHeight(this.props.location)]
|
||||
}
|
||||
return this.size
|
||||
}
|
||||
|
||||
update (newProps) {
|
||||
this.props = Object.assign({}, this.props, newProps)
|
||||
return etch.update(this)
|
||||
}
|
||||
|
||||
handleMouseDown (event) {
|
||||
if (event.detail === 2) {
|
||||
this.props.onResizeToFit()
|
||||
} else if (this.props.dockIsVisible) {
|
||||
this.props.onResizeStart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DockToggleButton {
|
||||
constructor (props) {
|
||||
this.props = props
|
||||
etch.initialize(this)
|
||||
}
|
||||
|
||||
render () {
|
||||
const classList = ['atom-dock-toggle-button', this.props.location]
|
||||
if (this.props.visible) classList.push(TOGGLE_BUTTON_VISIBLE_CLASS)
|
||||
|
||||
return $.div(
|
||||
{className: classList.join(' ')},
|
||||
$.div(
|
||||
{
|
||||
ref: 'innerElement',
|
||||
className: `atom-dock-toggle-button-inner ${this.props.location}`,
|
||||
on: {
|
||||
click: this.handleClick,
|
||||
dragenter: this.props.onDragEnter
|
||||
}
|
||||
},
|
||||
$.span({
|
||||
ref: 'iconElement',
|
||||
className: `icon ${getIconName(
|
||||
this.props.location,
|
||||
this.props.dockIsVisible
|
||||
)}`
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
getElement () {
|
||||
return this.element
|
||||
}
|
||||
|
||||
getBounds () {
|
||||
return this.refs.innerElement.getBoundingClientRect()
|
||||
}
|
||||
|
||||
update (newProps) {
|
||||
this.props = Object.assign({}, this.props, newProps)
|
||||
return etch.update(this)
|
||||
}
|
||||
|
||||
handleClick () {
|
||||
this.props.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
// An etch component that doesn't use etch, this component provides a gateway from JSX back into
|
||||
// the mutable DOM world.
|
||||
class ElementComponent {
|
||||
constructor (props) {
|
||||
this.element = props.element
|
||||
}
|
||||
|
||||
update (props) {
|
||||
this.element = props.element
|
||||
}
|
||||
}
|
||||
|
||||
function getWidthOrHeight (location) {
|
||||
return location === 'left' || location === 'right' ? 'width' : 'height'
|
||||
}
|
||||
|
||||
function getPreferredSize (item, location) {
|
||||
switch (location) {
|
||||
case 'left':
|
||||
case 'right':
|
||||
return typeof item.getPreferredWidth === 'function'
|
||||
? item.getPreferredWidth()
|
||||
: null
|
||||
default:
|
||||
return typeof item.getPreferredHeight === 'function'
|
||||
? item.getPreferredHeight()
|
||||
: null
|
||||
}
|
||||
}
|
||||
|
||||
function getIconName (location, visible) {
|
||||
switch (location) {
|
||||
case 'right': return visible ? 'icon-chevron-right' : 'icon-chevron-left'
|
||||
case 'bottom': return visible ? 'icon-chevron-down' : 'icon-chevron-up'
|
||||
case 'left': return visible ? 'icon-chevron-left' : 'icon-chevron-right'
|
||||
default: throw new Error(`Invalid location: ${location}`)
|
||||
}
|
||||
}
|
||||
|
||||
function rectContainsPoint (rect, point) {
|
||||
return (
|
||||
point.x >= rect.left &&
|
||||
point.y >= rect.top &&
|
||||
point.x <= rect.right &&
|
||||
point.y <= rect.bottom
|
||||
)
|
||||
}
|
||||
|
||||
// Is the item allowed in the given location?
|
||||
function isItemAllowed (item, location) {
|
||||
if (typeof item.getAllowedLocations !== 'function') return true
|
||||
return item.getAllowedLocations().includes(location)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
module.exports =
|
||||
class DOMElementPool
|
||||
constructor: ->
|
||||
@freeElementsByTagName = {}
|
||||
@freedElements = new Set
|
||||
|
||||
clear: ->
|
||||
@freedElements.clear()
|
||||
for tagName, freeElements of @freeElementsByTagName
|
||||
freeElements.length = 0
|
||||
return
|
||||
|
||||
build: (tagName, factory, reset) ->
|
||||
element = @freeElementsByTagName[tagName]?.pop()
|
||||
element ?= factory()
|
||||
reset(element)
|
||||
@freedElements.delete(element)
|
||||
element
|
||||
|
||||
buildElement: (tagName, className) ->
|
||||
factory = -> document.createElement(tagName)
|
||||
reset = (element) ->
|
||||
delete element.dataset[dataId] for dataId of element.dataset
|
||||
element.removeAttribute("style")
|
||||
if className?
|
||||
element.className = className
|
||||
else
|
||||
element.removeAttribute("class")
|
||||
@build(tagName, factory, reset)
|
||||
|
||||
buildText: (textContent) ->
|
||||
factory = -> document.createTextNode(textContent)
|
||||
reset = (element) -> element.textContent = textContent
|
||||
@build("#text", factory, reset)
|
||||
|
||||
freeElementAndDescendants: (element) ->
|
||||
@free(element)
|
||||
@freeDescendants(element)
|
||||
|
||||
freeDescendants: (element) ->
|
||||
for descendant in element.childNodes by -1
|
||||
@free(descendant)
|
||||
@freeDescendants(descendant)
|
||||
return
|
||||
|
||||
free: (element) ->
|
||||
throw new Error("The element cannot be null or undefined.") unless element?
|
||||
throw new Error("The element has already been freed!") if @freedElements.has(element)
|
||||
|
||||
tagName = element.nodeName.toLowerCase()
|
||||
@freeElementsByTagName[tagName] ?= []
|
||||
@freeElementsByTagName[tagName].push(element)
|
||||
@freedElements.add(element)
|
||||
|
||||
element.remove()
|
||||
80
src/electron-shims.js
Normal file
80
src/electron-shims.js
Normal file
@@ -0,0 +1,80 @@
|
||||
const path = require('path')
|
||||
const electron = require('electron')
|
||||
|
||||
const dirname = path.dirname
|
||||
path.dirname = function (path) {
|
||||
if (typeof path !== 'string') {
|
||||
path = '' + path
|
||||
const Grim = require('grim')
|
||||
Grim.deprecate('Argument to `path.dirname` must be a string')
|
||||
}
|
||||
|
||||
return dirname(path)
|
||||
}
|
||||
|
||||
const extname = path.extname
|
||||
path.extname = function (path) {
|
||||
if (typeof path !== 'string') {
|
||||
path = '' + path
|
||||
const Grim = require('grim')
|
||||
Grim.deprecate('Argument to `path.extname` must be a string')
|
||||
}
|
||||
|
||||
return extname(path)
|
||||
}
|
||||
|
||||
const basename = path.basename
|
||||
path.basename = function (path, ext) {
|
||||
if (typeof path !== 'string' || (ext !== undefined && typeof ext !== 'string')) {
|
||||
path = '' + path
|
||||
const Grim = require('grim')
|
||||
Grim.deprecate('Arguments to `path.basename` must be strings')
|
||||
}
|
||||
|
||||
return basename(path, ext)
|
||||
}
|
||||
|
||||
electron.ipcRenderer.sendChannel = function () {
|
||||
const Grim = require('grim')
|
||||
Grim.deprecate('Use `ipcRenderer.send` instead of `ipcRenderer.sendChannel`')
|
||||
return this.send.apply(this, arguments)
|
||||
}
|
||||
|
||||
const remoteRequire = electron.remote.require
|
||||
electron.remote.require = function (moduleName) {
|
||||
const Grim = require('grim')
|
||||
switch (moduleName) {
|
||||
case 'menu':
|
||||
Grim.deprecate('Use `remote.Menu` instead of `remote.require("menu")`')
|
||||
return this.Menu
|
||||
case 'menu-item':
|
||||
Grim.deprecate('Use `remote.MenuItem` instead of `remote.require("menu-item")`')
|
||||
return this.MenuItem
|
||||
case 'browser-window':
|
||||
Grim.deprecate('Use `remote.BrowserWindow` instead of `remote.require("browser-window")`')
|
||||
return this.BrowserWindow
|
||||
case 'dialog':
|
||||
Grim.deprecate('Use `remote.Dialog` instead of `remote.require("dialog")`')
|
||||
return this.Dialog
|
||||
case 'app':
|
||||
Grim.deprecate('Use `remote.app` instead of `remote.require("app")`')
|
||||
return this.app
|
||||
case 'crash-reporter':
|
||||
Grim.deprecate('Use `remote.crashReporter` instead of `remote.require("crashReporter")`')
|
||||
return this.crashReporter
|
||||
case 'global-shortcut':
|
||||
Grim.deprecate('Use `remote.globalShortcut` instead of `remote.require("global-shortcut")`')
|
||||
return this.globalShortcut
|
||||
case 'clipboard':
|
||||
Grim.deprecate('Use `remote.clipboard` instead of `remote.require("clipboard")`')
|
||||
return this.clipboard
|
||||
case 'native-image':
|
||||
Grim.deprecate('Use `remote.nativeImage` instead of `remote.require("native-image")`')
|
||||
return this.nativeImage
|
||||
case 'tray':
|
||||
Grim.deprecate('Use `remote.Tray` instead of `remote.require("tray")`')
|
||||
return this.Tray
|
||||
default:
|
||||
return remoteRequire.call(this, moduleName)
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
'use babel'
|
||||
|
||||
import {spawnSync} from 'child_process'
|
||||
import os from 'os'
|
||||
|
||||
// Gets a dump of the user's configured shell environment.
|
||||
//
|
||||
// Returns the output of the `env` command or `undefined` if there was an error.
|
||||
function getRawShellEnv () {
|
||||
let shell = getUserShell()
|
||||
|
||||
// The `-ilc` set of options was tested to work with the OS X v10.11
|
||||
// default-installed versions of bash, zsh, sh, and ksh. It *does not*
|
||||
// work with csh or tcsh.
|
||||
let results = spawnSync(shell, ['-ilc', 'env'], {encoding: 'utf8'})
|
||||
if (results.error || !results.stdout || results.stdout.length <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
return results.stdout
|
||||
}
|
||||
|
||||
function getUserShell () {
|
||||
if (process.env.SHELL) {
|
||||
return process.env.SHELL
|
||||
}
|
||||
|
||||
return '/bin/bash'
|
||||
}
|
||||
|
||||
// Gets the user's configured shell environment.
|
||||
//
|
||||
// Returns a copy of the user's shell enviroment.
|
||||
function getFromShell () {
|
||||
let shellEnvText = getRawShellEnv()
|
||||
if (!shellEnvText) {
|
||||
return
|
||||
}
|
||||
|
||||
let env = {}
|
||||
|
||||
for (let line of shellEnvText.split(os.EOL)) {
|
||||
if (line.includes('=')) {
|
||||
let components = line.split('=')
|
||||
if (components.length === 2) {
|
||||
env[components[0]] = components[1]
|
||||
} else {
|
||||
let k = components.shift()
|
||||
let v = components.join('=')
|
||||
env[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
function needsPatching (options = { platform: process.platform, env: process.env }) {
|
||||
if (options.platform === 'darwin' && !options.env.PWD) {
|
||||
let shell = getUserShell()
|
||||
if (shell.endsWith('csh') || shell.endsWith('tcsh')) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function normalize (options = {}) {
|
||||
if (options && options.env) {
|
||||
process.env = options.env
|
||||
}
|
||||
|
||||
if (!options.env) {
|
||||
options.env = process.env
|
||||
}
|
||||
|
||||
if (!options.platform) {
|
||||
options.platform = process.platform
|
||||
}
|
||||
|
||||
if (needsPatching(options)) {
|
||||
// Patch the `process.env` on startup to fix the problem first documented
|
||||
// in #4126. Retain the original in case someone needs it.
|
||||
let shellEnv = getFromShell()
|
||||
if (shellEnv && shellEnv.PATH) {
|
||||
process._originalEnv = process.env
|
||||
process.env = shellEnv
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default { getFromShell, needsPatching, normalize }
|
||||
@@ -14,16 +14,15 @@ class FileSystemBlobStore {
|
||||
constructor (directory) {
|
||||
this.blobFilename = path.join(directory, 'BLOB')
|
||||
this.blobMapFilename = path.join(directory, 'MAP')
|
||||
this.invalidationKeysFilename = path.join(directory, 'INVKEYS')
|
||||
this.lockFilename = path.join(directory, 'LOCK')
|
||||
this.reset()
|
||||
}
|
||||
|
||||
reset () {
|
||||
this.inMemoryBlobs = new Map()
|
||||
this.invalidationKeys = {}
|
||||
this.storedBlob = new Buffer(0)
|
||||
this.storedBlobMap = {}
|
||||
this.usedKeys = new Set()
|
||||
}
|
||||
|
||||
load () {
|
||||
@@ -33,14 +32,10 @@ class FileSystemBlobStore {
|
||||
if (!fs.existsSync(this.blobFilename)) {
|
||||
return
|
||||
}
|
||||
if (!fs.existsSync(this.invalidationKeysFilename)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.storedBlob = fs.readFileSync(this.blobFilename)
|
||||
this.storedBlobMap = JSON.parse(fs.readFileSync(this.blobMapFilename))
|
||||
this.invalidationKeys = JSON.parse(fs.readFileSync(this.invalidationKeysFilename))
|
||||
} catch (e) {
|
||||
this.reset()
|
||||
}
|
||||
@@ -50,7 +45,6 @@ class FileSystemBlobStore {
|
||||
let dump = this.getDump()
|
||||
let blobToStore = Buffer.concat(dump[0])
|
||||
let mapToStore = JSON.stringify(dump[1])
|
||||
let invalidationKeysToStore = JSON.stringify(this.invalidationKeys)
|
||||
|
||||
let acquiredLock = false
|
||||
try {
|
||||
@@ -59,7 +53,6 @@ class FileSystemBlobStore {
|
||||
|
||||
fs.writeFileSync(this.blobFilename, blobToStore)
|
||||
fs.writeFileSync(this.blobMapFilename, mapToStore)
|
||||
fs.writeFileSync(this.invalidationKeysFilename, invalidationKeysToStore)
|
||||
} catch (error) {
|
||||
// Swallow the exception silently only if we fail to acquire the lock.
|
||||
if (error.code !== 'EEXIST') {
|
||||
@@ -72,20 +65,19 @@ class FileSystemBlobStore {
|
||||
}
|
||||
}
|
||||
|
||||
has (key, invalidationKey) {
|
||||
let containsKey = this.inMemoryBlobs.has(key) || this.storedBlobMap.hasOwnProperty(key)
|
||||
let isValid = this.invalidationKeys[key] === invalidationKey
|
||||
return containsKey && isValid
|
||||
has (key) {
|
||||
return this.inMemoryBlobs.has(key) || this.storedBlobMap.hasOwnProperty(key)
|
||||
}
|
||||
|
||||
get (key, invalidationKey) {
|
||||
if (this.has(key, invalidationKey)) {
|
||||
get (key) {
|
||||
if (this.has(key)) {
|
||||
this.usedKeys.add(key)
|
||||
return this.getFromMemory(key) || this.getFromStorage(key)
|
||||
}
|
||||
}
|
||||
|
||||
set (key, invalidationKey, buffer) {
|
||||
this.invalidationKeys[key] = invalidationKey
|
||||
set (key, buffer) {
|
||||
this.usedKeys.add(key)
|
||||
return this.inMemoryBlobs.set(key, buffer)
|
||||
}
|
||||
|
||||
@@ -119,11 +111,13 @@ class FileSystemBlobStore {
|
||||
}
|
||||
|
||||
for (let key of this.inMemoryBlobs.keys()) {
|
||||
dump(key, this.getFromMemory.bind(this))
|
||||
if (this.usedKeys.has(key)) {
|
||||
dump(key, this.getFromMemory.bind(this))
|
||||
}
|
||||
}
|
||||
|
||||
for (let key of Object.keys(this.storedBlobMap)) {
|
||||
if (!blobMap[key]) {
|
||||
if (!blobMap[key] && this.usedKeys.has(key)) {
|
||||
dump(key, this.getFromStorage.bind(this))
|
||||
}
|
||||
}
|
||||
|
||||
11
src/first-mate-helpers.js
Normal file
11
src/first-mate-helpers.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
fromFirstMateScopeId (firstMateScopeId) {
|
||||
let atomScopeId = -firstMateScopeId
|
||||
if ((atomScopeId & 1) === 0) atomScopeId--
|
||||
return atomScopeId + 256
|
||||
},
|
||||
|
||||
toFirstMateScopeId (atomScopeId) {
|
||||
return -(atomScopeId - 256)
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
{Point, Range} = require 'text-buffer'
|
||||
|
||||
# Represents a fold that collapses multiple buffer lines into a single
|
||||
# line on the screen.
|
||||
#
|
||||
# Their creation is managed by the {DisplayBuffer}.
|
||||
module.exports =
|
||||
class Fold
|
||||
id: null
|
||||
displayBuffer: null
|
||||
marker: null
|
||||
|
||||
constructor: (@displayBuffer, @marker) ->
|
||||
@id = @marker.id
|
||||
@displayBuffer.foldsByMarkerId[@marker.id] = this
|
||||
@marker.onDidDestroy => @destroyed()
|
||||
@marker.onDidChange ({isValid}) => @destroy() unless isValid
|
||||
|
||||
# Returns whether this fold is contained within another fold
|
||||
isInsideLargerFold: ->
|
||||
largestContainingFoldMarker = @displayBuffer.findFoldMarker(containsRange: @getBufferRange())
|
||||
largestContainingFoldMarker and
|
||||
not largestContainingFoldMarker.getRange().isEqual(@getBufferRange())
|
||||
|
||||
# Destroys this fold
|
||||
destroy: ->
|
||||
@marker.destroy()
|
||||
|
||||
# Returns the fold's {Range} in buffer coordinates
|
||||
#
|
||||
# includeNewline - A {Boolean} which, if `true`, includes the trailing newline
|
||||
#
|
||||
# Returns a {Range}.
|
||||
getBufferRange: ({includeNewline}={}) ->
|
||||
range = @marker.getRange()
|
||||
|
||||
if range.end.row > range.start.row and nextFold = @displayBuffer.largestFoldStartingAtBufferRow(range.end.row)
|
||||
nextRange = nextFold.getBufferRange()
|
||||
range = new Range(range.start, nextRange.end)
|
||||
|
||||
if includeNewline
|
||||
range = range.copy()
|
||||
range.end.row++
|
||||
range.end.column = 0
|
||||
range
|
||||
|
||||
getBufferRowRange: ->
|
||||
{start, end} = @getBufferRange()
|
||||
[start.row, end.row]
|
||||
|
||||
# Returns the fold's start row as a {Number}.
|
||||
getStartRow: ->
|
||||
@getBufferRange().start.row
|
||||
|
||||
# Returns the fold's end row as a {Number}.
|
||||
getEndRow: ->
|
||||
@getBufferRange().end.row
|
||||
|
||||
# Returns a {String} representation of the fold.
|
||||
inspect: ->
|
||||
"Fold(#{@getStartRow()}, #{@getEndRow()})"
|
||||
|
||||
# Retrieves the number of buffer rows spanned by the fold.
|
||||
#
|
||||
# Returns a {Number}.
|
||||
getBufferRowCount: ->
|
||||
@getEndRow() - @getStartRow() + 1
|
||||
|
||||
# Identifies if a fold is nested within a fold.
|
||||
#
|
||||
# fold - A {Fold} to check
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
isContainedByFold: (fold) ->
|
||||
@isContainedByRange(fold.getBufferRange())
|
||||
|
||||
updateDisplayBuffer: ->
|
||||
unless @isInsideLargerFold()
|
||||
@displayBuffer.updateScreenLines(@getStartRow(), @getEndRow() + 1, 0, updateMarkers: true)
|
||||
|
||||
destroyed: ->
|
||||
delete @displayBuffer.foldsByMarkerId[@marker.id]
|
||||
@updateDisplayBuffer()
|
||||
10
src/get-window-load-settings.js
Normal file
10
src/get-window-load-settings.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const {remote} = require('electron')
|
||||
|
||||
let windowLoadSettings = null
|
||||
|
||||
module.exports = () => {
|
||||
if (!windowLoadSettings) {
|
||||
windowLoadSettings = JSON.parse(remote.getCurrentWindow().loadSettingsJSON)
|
||||
}
|
||||
return windowLoadSettings
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -77,7 +77,7 @@ class GitRepositoryProvider
|
||||
unless repo
|
||||
repo = GitRepository.open(gitDirPath, {@project, @config})
|
||||
return null unless repo
|
||||
repo.async.onDidDestroy(=> delete @pathToRepository[gitDirPath])
|
||||
repo.onDidDestroy(=> delete @pathToRepository[gitDirPath])
|
||||
@pathToRepository[gitDirPath] = repo
|
||||
repo.refreshIndex()
|
||||
repo.refreshStatus()
|
||||
|
||||
@@ -1,509 +0,0 @@
|
||||
{basename, join} = require 'path'
|
||||
|
||||
_ = require 'underscore-plus'
|
||||
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
|
||||
fs = require 'fs-plus'
|
||||
GitRepositoryAsync = require './git-repository-async'
|
||||
GitUtils = require 'git-utils'
|
||||
|
||||
Task = require './task'
|
||||
|
||||
# Extended: Represents the underlying git operations performed by Atom.
|
||||
#
|
||||
# This class shouldn't be instantiated directly but instead by accessing the
|
||||
# `atom.project` global and calling `getRepositories()`. Note that this will
|
||||
# only be available when the project is backed by a Git repository.
|
||||
#
|
||||
# This class handles submodules automatically by taking a `path` argument to many
|
||||
# of the methods. This `path` argument will determine which underlying
|
||||
# repository is used.
|
||||
#
|
||||
# For a repository with submodules this would have the following outcome:
|
||||
#
|
||||
# ```coffee
|
||||
# repo = atom.project.getRepositories()[0]
|
||||
# repo.getShortHead() # 'master'
|
||||
# repo.getShortHead('vendor/path/to/a/submodule') # 'dead1234'
|
||||
# ```
|
||||
#
|
||||
# ## Examples
|
||||
#
|
||||
# ### Logging the URL of the origin remote
|
||||
#
|
||||
# ```coffee
|
||||
# git = atom.project.getRepositories()[0]
|
||||
# console.log git.getOriginURL()
|
||||
# ```
|
||||
#
|
||||
# ### Requiring in packages
|
||||
#
|
||||
# ```coffee
|
||||
# {GitRepository} = require 'atom'
|
||||
# ```
|
||||
module.exports =
|
||||
class GitRepository
|
||||
@exists: (path) ->
|
||||
if git = @open(path)
|
||||
git.destroy()
|
||||
true
|
||||
else
|
||||
false
|
||||
|
||||
###
|
||||
Section: Construction and Destruction
|
||||
###
|
||||
|
||||
# Public: Creates a new GitRepository instance.
|
||||
#
|
||||
# * `path` The {String} path to the Git repository to open.
|
||||
# * `options` An optional {Object} with the following keys:
|
||||
# * `refreshOnWindowFocus` A {Boolean}, `true` to refresh the index and
|
||||
# statuses when the window is focused.
|
||||
#
|
||||
# Returns a {GitRepository} instance or `null` if the repository could not be opened.
|
||||
@open: (path, options) ->
|
||||
return null unless path
|
||||
try
|
||||
new GitRepository(path, options)
|
||||
catch
|
||||
null
|
||||
|
||||
constructor: (path, options={}) ->
|
||||
@emitter = new Emitter
|
||||
@subscriptions = new CompositeDisposable
|
||||
|
||||
@repo = GitUtils.open(path)
|
||||
unless @repo?
|
||||
throw new Error("No Git repository found searching path: #{path}")
|
||||
|
||||
asyncOptions = _.clone(options)
|
||||
# GitRepository itself will handle these cases by manually calling through
|
||||
# to the async repo.
|
||||
asyncOptions.refreshOnWindowFocus = false
|
||||
asyncOptions.subscribeToBuffers = false
|
||||
@async = GitRepositoryAsync.open(path, asyncOptions)
|
||||
|
||||
@statuses = {}
|
||||
@upstream = {ahead: 0, behind: 0}
|
||||
for submodulePath, submoduleRepo of @repo.submodules
|
||||
submoduleRepo.upstream = {ahead: 0, behind: 0}
|
||||
|
||||
{@project, @config, refreshOnWindowFocus} = options
|
||||
|
||||
refreshOnWindowFocus ?= true
|
||||
if refreshOnWindowFocus
|
||||
onWindowFocus = =>
|
||||
@refreshIndex()
|
||||
@refreshStatus()
|
||||
|
||||
window.addEventListener 'focus', onWindowFocus
|
||||
@subscriptions.add new Disposable(-> window.removeEventListener 'focus', onWindowFocus)
|
||||
|
||||
if @project?
|
||||
@project.getBuffers().forEach (buffer) => @subscribeToBuffer(buffer)
|
||||
@subscriptions.add @project.onDidAddBuffer (buffer) => @subscribeToBuffer(buffer)
|
||||
|
||||
# Public: Destroy this {GitRepository} object.
|
||||
#
|
||||
# This destroys any tasks and subscriptions and releases the underlying
|
||||
# libgit2 repository handle. This method is idempotent.
|
||||
destroy: ->
|
||||
if @emitter?
|
||||
@emitter.emit 'did-destroy'
|
||||
@emitter.dispose()
|
||||
@emitter = null
|
||||
|
||||
if @statusTask?
|
||||
@statusTask.terminate()
|
||||
@statusTask = null
|
||||
|
||||
if @repo?
|
||||
@repo.release()
|
||||
@repo = null
|
||||
|
||||
if @subscriptions?
|
||||
@subscriptions.dispose()
|
||||
@subscriptions = null
|
||||
|
||||
if @async?
|
||||
@async.destroy()
|
||||
@async = null
|
||||
|
||||
# Public: Invoke the given callback when this GitRepository's destroy() method
|
||||
# is invoked.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy: (callback) ->
|
||||
@emitter.on 'did-destroy', callback
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
###
|
||||
|
||||
# Public: Invoke the given callback when a specific file's status has
|
||||
# changed. When a file is updated, reloaded, etc, and the status changes, this
|
||||
# will be fired.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
# * `event` {Object}
|
||||
# * `path` {String} the old parameters the decoration used to have
|
||||
# * `pathStatus` {Number} representing the status. This value can be passed to
|
||||
# {::isStatusModified} or {::isStatusNew} to get more information.
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeStatus: (callback) ->
|
||||
@emitter.on 'did-change-status', callback
|
||||
|
||||
# Public: Invoke the given callback when a multiple files' statuses have
|
||||
# changed. For example, on window focus, the status of all the paths in the
|
||||
# repo is checked. If any of them have changed, this will be fired. Call
|
||||
# {::getPathStatus(path)} to get the status for your path of choice.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeStatuses: (callback) ->
|
||||
@emitter.on 'did-change-statuses', callback
|
||||
|
||||
###
|
||||
Section: Repository Details
|
||||
###
|
||||
|
||||
# Public: A {String} indicating the type of version control system used by
|
||||
# this repository.
|
||||
#
|
||||
# Returns `"git"`.
|
||||
getType: -> 'git'
|
||||
|
||||
# Public: Returns the {String} path of the repository.
|
||||
getPath: ->
|
||||
@path ?= fs.absolute(@getRepo().getPath())
|
||||
|
||||
# Public: Returns the {String} working directory path of the repository.
|
||||
getWorkingDirectory: -> @getRepo().getWorkingDirectory()
|
||||
|
||||
# Public: Returns true if at the root, false if in a subfolder of the
|
||||
# repository.
|
||||
isProjectAtRoot: ->
|
||||
@projectAtRoot ?= @project?.relativize(@getWorkingDirectory()) is ''
|
||||
|
||||
# Public: Makes a path relative to the repository's working directory.
|
||||
relativize: (path) -> @getRepo().relativize(path)
|
||||
|
||||
# Public: Returns true if the given branch exists.
|
||||
hasBranch: (branch) -> @getReferenceTarget("refs/heads/#{branch}")?
|
||||
|
||||
# Public: Retrieves a shortened version of the HEAD reference value.
|
||||
#
|
||||
# This removes the leading segments of `refs/heads`, `refs/tags`, or
|
||||
# `refs/remotes`. It also shortens the SHA-1 of a detached `HEAD` to 7
|
||||
# characters.
|
||||
#
|
||||
# * `path` An optional {String} path in the repository to get this information
|
||||
# for, only needed if the repository contains submodules.
|
||||
#
|
||||
# Returns a {String}.
|
||||
getShortHead: (path) -> @getRepo(path).getShortHead()
|
||||
|
||||
# Public: Is the given path a submodule in the repository?
|
||||
#
|
||||
# * `path` The {String} path to check.
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
isSubmodule: (path) ->
|
||||
return false unless path
|
||||
|
||||
repo = @getRepo(path)
|
||||
if repo.isSubmodule(repo.relativize(path))
|
||||
true
|
||||
else
|
||||
# Check if the path is a working directory in a repo that isn't the root.
|
||||
repo isnt @getRepo() and repo.relativize(join(path, 'dir')) is 'dir'
|
||||
|
||||
# Public: Returns the number of commits behind the current branch is from the
|
||||
# its upstream remote branch.
|
||||
#
|
||||
# * `reference` The {String} branch reference name.
|
||||
# * `path` The {String} path in the repository to get this information for,
|
||||
# only needed if the repository contains submodules.
|
||||
getAheadBehindCount: (reference, path) ->
|
||||
@getRepo(path).getAheadBehindCount(reference)
|
||||
|
||||
# Public: Get the cached ahead/behind commit counts for the current branch's
|
||||
# upstream branch.
|
||||
#
|
||||
# * `path` An optional {String} path in the repository to get this information
|
||||
# for, only needed if the repository has submodules.
|
||||
#
|
||||
# Returns an {Object} with the following keys:
|
||||
# * `ahead` The {Number} of commits ahead.
|
||||
# * `behind` The {Number} of commits behind.
|
||||
getCachedUpstreamAheadBehindCount: (path) ->
|
||||
@getRepo(path).upstream ? @upstream
|
||||
|
||||
# Public: Returns the git configuration value specified by the key.
|
||||
#
|
||||
# * `path` An optional {String} path in the repository to get this information
|
||||
# for, only needed if the repository has submodules.
|
||||
getConfigValue: (key, path) -> @getRepo(path).getConfigValue(key)
|
||||
|
||||
# Public: Returns the origin url of the repository.
|
||||
#
|
||||
# * `path` (optional) {String} path in the repository to get this information
|
||||
# for, only needed if the repository has submodules.
|
||||
getOriginURL: (path) -> @getConfigValue('remote.origin.url', path)
|
||||
|
||||
# Public: Returns the upstream branch for the current HEAD, or null if there
|
||||
# is no upstream branch for the current HEAD.
|
||||
#
|
||||
# * `path` An optional {String} path in the repo to get this information for,
|
||||
# only needed if the repository contains submodules.
|
||||
#
|
||||
# Returns a {String} branch name such as `refs/remotes/origin/master`.
|
||||
getUpstreamBranch: (path) -> @getRepo(path).getUpstreamBranch()
|
||||
|
||||
# Public: Gets all the local and remote references.
|
||||
#
|
||||
# * `path` An optional {String} path in the repository to get this information
|
||||
# for, only needed if the repository has submodules.
|
||||
#
|
||||
# Returns an {Object} with the following keys:
|
||||
# * `heads` An {Array} of head reference names.
|
||||
# * `remotes` An {Array} of remote reference names.
|
||||
# * `tags` An {Array} of tag reference names.
|
||||
getReferences: (path) -> @getRepo(path).getReferences()
|
||||
|
||||
# Public: Returns the current {String} SHA for the given reference.
|
||||
#
|
||||
# * `reference` The {String} reference to get the target of.
|
||||
# * `path` An optional {String} path in the repo to get the reference target
|
||||
# for. Only needed if the repository contains submodules.
|
||||
getReferenceTarget: (reference, path) ->
|
||||
@getRepo(path).getReferenceTarget(reference)
|
||||
|
||||
###
|
||||
Section: Reading Status
|
||||
###
|
||||
|
||||
# Public: Returns true if the given path is modified.
|
||||
#
|
||||
# * `path` The {String} path to check.
|
||||
#
|
||||
# Returns a {Boolean} that's true if the `path` is modified.
|
||||
isPathModified: (path) -> @isStatusModified(@getPathStatus(path))
|
||||
|
||||
# Public: Returns true if the given path is new.
|
||||
#
|
||||
# * `path` The {String} path to check.
|
||||
#
|
||||
# Returns a {Boolean} that's true if the `path` is new.
|
||||
isPathNew: (path) -> @isStatusNew(@getPathStatus(path))
|
||||
|
||||
# Public: Is the given path ignored?
|
||||
#
|
||||
# * `path` The {String} path to check.
|
||||
#
|
||||
# Returns a {Boolean} that's true if the `path` is ignored.
|
||||
isPathIgnored: (path) -> @getRepo().isIgnored(@relativize(path))
|
||||
|
||||
# Public: Get the status of a directory in the repository's working directory.
|
||||
#
|
||||
# * `path` The {String} path to check.
|
||||
#
|
||||
# Returns a {Number} representing the status. This value can be passed to
|
||||
# {::isStatusModified} or {::isStatusNew} to get more information.
|
||||
getDirectoryStatus: (directoryPath) ->
|
||||
directoryPath = "#{@relativize(directoryPath)}/"
|
||||
directoryStatus = 0
|
||||
for path, status of @statuses
|
||||
directoryStatus |= status if path.indexOf(directoryPath) is 0
|
||||
directoryStatus
|
||||
|
||||
# Public: Get the status of a single path in the repository.
|
||||
#
|
||||
# `path` A {String} repository-relative path.
|
||||
#
|
||||
# Returns a {Number} representing the status. This value can be passed to
|
||||
# {::isStatusModified} or {::isStatusNew} to get more information.
|
||||
getPathStatus: (path) ->
|
||||
# Trigger events emitted on the async repo as well
|
||||
@async.refreshStatusForPath(path)
|
||||
|
||||
repo = @getRepo(path)
|
||||
relativePath = @relativize(path)
|
||||
currentPathStatus = @statuses[relativePath] ? 0
|
||||
pathStatus = repo.getStatus(repo.relativize(path)) ? 0
|
||||
pathStatus = 0 if repo.isStatusIgnored(pathStatus)
|
||||
if pathStatus > 0
|
||||
@statuses[relativePath] = pathStatus
|
||||
else
|
||||
delete @statuses[relativePath]
|
||||
if currentPathStatus isnt pathStatus
|
||||
@emitter.emit 'did-change-status', {path, pathStatus}
|
||||
|
||||
pathStatus
|
||||
|
||||
# Public: Get the cached status for the given path.
|
||||
#
|
||||
# * `path` A {String} path in the repository, relative or absolute.
|
||||
#
|
||||
# Returns a status {Number} or null if the path is not in the cache.
|
||||
getCachedPathStatus: (path) ->
|
||||
@statuses[@relativize(path)]
|
||||
|
||||
# Public: Returns true if the given status indicates modification.
|
||||
#
|
||||
# * `status` A {Number} representing the status.
|
||||
#
|
||||
# Returns a {Boolean} that's true if the `status` indicates modification.
|
||||
isStatusModified: (status) -> @getRepo().isStatusModified(status)
|
||||
|
||||
# Public: Returns true if the given status indicates a new path.
|
||||
#
|
||||
# * `status` A {Number} representing the status.
|
||||
#
|
||||
# Returns a {Boolean} that's true if the `status` indicates a new path.
|
||||
isStatusNew: (status) -> @getRepo().isStatusNew(status)
|
||||
|
||||
###
|
||||
Section: Retrieving Diffs
|
||||
###
|
||||
|
||||
# Public: Retrieves the number of lines added and removed to a path.
|
||||
#
|
||||
# This compares the working directory contents of the path to the `HEAD`
|
||||
# version.
|
||||
#
|
||||
# * `path` The {String} path to check.
|
||||
#
|
||||
# Returns an {Object} with the following keys:
|
||||
# * `added` The {Number} of added lines.
|
||||
# * `deleted` The {Number} of deleted lines.
|
||||
getDiffStats: (path) ->
|
||||
repo = @getRepo(path)
|
||||
repo.getDiffStats(repo.relativize(path))
|
||||
|
||||
# Public: Retrieves the line diffs comparing the `HEAD` version of the given
|
||||
# path and the given text.
|
||||
#
|
||||
# * `path` The {String} path relative to the repository.
|
||||
# * `text` The {String} to compare against the `HEAD` contents
|
||||
#
|
||||
# Returns an {Array} of hunk {Object}s with the following keys:
|
||||
# * `oldStart` The line {Number} of the old hunk.
|
||||
# * `newStart` The line {Number} of the new hunk.
|
||||
# * `oldLines` The {Number} of lines in the old hunk.
|
||||
# * `newLines` The {Number} of lines in the new hunk
|
||||
getLineDiffs: (path, text) ->
|
||||
# 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 = ignoreEolWhitespace: process.platform is 'win32'
|
||||
repo = @getRepo(path)
|
||||
repo.getLineDiffs(repo.relativize(path), text, options)
|
||||
|
||||
###
|
||||
Section: Checking Out
|
||||
###
|
||||
|
||||
# Public: Restore the contents of a path in the working directory and index
|
||||
# to the version at `HEAD`.
|
||||
#
|
||||
# This is essentially the same as running:
|
||||
#
|
||||
# ```sh
|
||||
# git reset HEAD -- <path>
|
||||
# git checkout HEAD -- <path>
|
||||
# ```
|
||||
#
|
||||
# * `path` The {String} path to checkout.
|
||||
#
|
||||
# Returns a {Boolean} that's true if the method was successful.
|
||||
checkoutHead: (path) ->
|
||||
repo = @getRepo(path)
|
||||
headCheckedOut = repo.checkoutHead(repo.relativize(path))
|
||||
@getPathStatus(path) if headCheckedOut
|
||||
headCheckedOut
|
||||
|
||||
# Public: Checks out a branch in your repository.
|
||||
#
|
||||
# * `reference` The {String} reference to checkout.
|
||||
# * `create` A {Boolean} value which, if true creates the new reference if
|
||||
# it doesn't exist.
|
||||
#
|
||||
# Returns a Boolean that's true if the method was successful.
|
||||
checkoutReference: (reference, create) ->
|
||||
@getRepo().checkoutReference(reference, create)
|
||||
|
||||
###
|
||||
Section: Private
|
||||
###
|
||||
|
||||
# Subscribes to buffer events.
|
||||
subscribeToBuffer: (buffer) ->
|
||||
getBufferPathStatus = =>
|
||||
if path = buffer.getPath()
|
||||
@getPathStatus(path)
|
||||
|
||||
bufferSubscriptions = new CompositeDisposable
|
||||
bufferSubscriptions.add buffer.onDidSave(getBufferPathStatus)
|
||||
bufferSubscriptions.add buffer.onDidReload(getBufferPathStatus)
|
||||
bufferSubscriptions.add buffer.onDidChangePath(getBufferPathStatus)
|
||||
bufferSubscriptions.add buffer.onDidDestroy =>
|
||||
bufferSubscriptions.dispose()
|
||||
@subscriptions.remove(bufferSubscriptions)
|
||||
@subscriptions.add(bufferSubscriptions)
|
||||
return
|
||||
|
||||
# Subscribes to editor view event.
|
||||
checkoutHeadForEditor: (editor) ->
|
||||
if filePath = editor.getPath()
|
||||
editor.buffer.reload() if editor.buffer.isModified()
|
||||
@checkoutHead(filePath)
|
||||
|
||||
# Returns the corresponding {Repository}
|
||||
getRepo: (path) ->
|
||||
if @repo?
|
||||
@repo.submoduleForPath(path) ? @repo
|
||||
else
|
||||
throw new Error("Repository has been destroyed")
|
||||
|
||||
# Reread the index to update any values that have changed since the
|
||||
# last time the index was read.
|
||||
refreshIndex: -> @getRepo().refreshIndex()
|
||||
|
||||
# Refreshes the current git status in an outside process and asynchronously
|
||||
# updates the relevant properties.
|
||||
#
|
||||
# Returns a promise that resolves when the repository has been refreshed.
|
||||
refreshStatus: ->
|
||||
asyncRefresh = @async.refreshStatus()
|
||||
syncRefresh = new Promise (resolve, reject) =>
|
||||
@handlerPath ?= require.resolve('./repository-status-handler')
|
||||
|
||||
relativeProjectPaths = @project?.getPaths()
|
||||
.map (path) => @relativize(path)
|
||||
.map (path) -> if path.length > 0 then path + '/**' else '*'
|
||||
|
||||
@statusTask?.terminate()
|
||||
@statusTask = Task.once @handlerPath, @getPath(), relativeProjectPaths, ({statuses, upstream, branch, submodules}) =>
|
||||
statusesUnchanged = _.isEqual(statuses, @statuses) and
|
||||
_.isEqual(upstream, @upstream) and
|
||||
_.isEqual(branch, @branch) and
|
||||
_.isEqual(submodules, @submodules)
|
||||
|
||||
@statuses = statuses
|
||||
@upstream = upstream
|
||||
@branch = branch
|
||||
@submodules = submodules
|
||||
|
||||
for submodulePath, submoduleRepo of @getRepo().submodules
|
||||
submoduleRepo.upstream = submodules[submodulePath]?.upstream ? {ahead: 0, behind: 0}
|
||||
|
||||
resolve()
|
||||
|
||||
unless statusesUnchanged
|
||||
@emitter.emit 'did-change-statuses'
|
||||
|
||||
return Promise.all([asyncRefresh, syncRefresh])
|
||||
595
src/git-repository.js
Normal file
595
src/git-repository.js
Normal file
@@ -0,0 +1,595 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs-plus')
|
||||
const _ = require('underscore-plus')
|
||||
const {Emitter, Disposable, CompositeDisposable} = require('event-kit')
|
||||
const GitUtils = require('git-utils')
|
||||
|
||||
let nextId = 0
|
||||
|
||||
// Extended: Represents the underlying git operations performed by Atom.
|
||||
//
|
||||
// This class shouldn't be instantiated directly but instead by accessing the
|
||||
// `atom.project` global and calling `getRepositories()`. Note that this will
|
||||
// only be available when the project is backed by a Git repository.
|
||||
//
|
||||
// This class handles submodules automatically by taking a `path` argument to many
|
||||
// of the methods. This `path` argument will determine which underlying
|
||||
// repository is used.
|
||||
//
|
||||
// For a repository with submodules this would have the following outcome:
|
||||
//
|
||||
// ```coffee
|
||||
// repo = atom.project.getRepositories()[0]
|
||||
// repo.getShortHead() # 'master'
|
||||
// repo.getShortHead('vendor/path/to/a/submodule') # 'dead1234'
|
||||
// ```
|
||||
//
|
||||
// ## Examples
|
||||
//
|
||||
// ### Logging the URL of the origin remote
|
||||
//
|
||||
// ```coffee
|
||||
// git = atom.project.getRepositories()[0]
|
||||
// console.log git.getOriginURL()
|
||||
// ```
|
||||
//
|
||||
// ### Requiring in packages
|
||||
//
|
||||
// ```coffee
|
||||
// {GitRepository} = require 'atom'
|
||||
// ```
|
||||
module.exports =
|
||||
class GitRepository {
|
||||
static exists (path) {
|
||||
const git = this.open(path)
|
||||
if (git) {
|
||||
git.destroy()
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Construction and Destruction
|
||||
*/
|
||||
|
||||
// Public: Creates a new GitRepository instance.
|
||||
//
|
||||
// * `path` The {String} path to the Git repository to open.
|
||||
// * `options` An optional {Object} with the following keys:
|
||||
// * `refreshOnWindowFocus` A {Boolean}, `true` to refresh the index and
|
||||
// statuses when the window is focused.
|
||||
//
|
||||
// Returns a {GitRepository} instance or `null` if the repository could not be opened.
|
||||
static open (path, options) {
|
||||
if (!path) { return null }
|
||||
try {
|
||||
return new GitRepository(path, options)
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
constructor (path, options = {}) {
|
||||
this.id = nextId++
|
||||
this.emitter = new Emitter()
|
||||
this.subscriptions = new CompositeDisposable()
|
||||
this.repo = GitUtils.open(path)
|
||||
if (this.repo == null) {
|
||||
throw new Error(`No Git repository found searching path: ${path}`)
|
||||
}
|
||||
|
||||
this.statusRefreshCount = 0
|
||||
this.statuses = {}
|
||||
this.upstream = {ahead: 0, behind: 0}
|
||||
for (let submodulePath in this.repo.submodules) {
|
||||
const submoduleRepo = this.repo.submodules[submodulePath]
|
||||
submoduleRepo.upstream = {ahead: 0, behind: 0}
|
||||
}
|
||||
|
||||
this.project = options.project
|
||||
this.config = options.config
|
||||
|
||||
if (options.refreshOnWindowFocus || options.refreshOnWindowFocus == null) {
|
||||
const onWindowFocus = () => {
|
||||
this.refreshIndex()
|
||||
this.refreshStatus()
|
||||
}
|
||||
|
||||
window.addEventListener('focus', onWindowFocus)
|
||||
this.subscriptions.add(new Disposable(() => window.removeEventListener('focus', onWindowFocus)))
|
||||
}
|
||||
|
||||
if (this.project != null) {
|
||||
this.project.getBuffers().forEach(buffer => this.subscribeToBuffer(buffer))
|
||||
this.subscriptions.add(this.project.onDidAddBuffer(buffer => this.subscribeToBuffer(buffer)))
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Destroy this {GitRepository} object.
|
||||
//
|
||||
// This destroys any tasks and subscriptions and releases the underlying
|
||||
// libgit2 repository handle. This method is idempotent.
|
||||
destroy () {
|
||||
this.repo = null
|
||||
|
||||
if (this.emitter) {
|
||||
this.emitter.emit('did-destroy')
|
||||
this.emitter.dispose()
|
||||
this.emitter = null
|
||||
}
|
||||
|
||||
if (this.subscriptions) {
|
||||
this.subscriptions.dispose()
|
||||
this.subscriptions = null
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Returns a {Boolean} indicating if this repository has been destroyed.
|
||||
isDestroyed () {
|
||||
return this.repo == null
|
||||
}
|
||||
|
||||
// Public: Invoke the given callback when this GitRepository's destroy() method
|
||||
// is invoked.
|
||||
//
|
||||
// * `callback` {Function}
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy (callback) {
|
||||
return this.emitter.once('did-destroy', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Event Subscription
|
||||
*/
|
||||
|
||||
// Public: Invoke the given callback when a specific file's status has
|
||||
// changed. When a file is updated, reloaded, etc, and the status changes, this
|
||||
// will be fired.
|
||||
//
|
||||
// * `callback` {Function}
|
||||
// * `event` {Object}
|
||||
// * `path` {String} the old parameters the decoration used to have
|
||||
// * `pathStatus` {Number} representing the status. This value can be passed to
|
||||
// {::isStatusModified} or {::isStatusNew} to get more information.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeStatus (callback) {
|
||||
return this.emitter.on('did-change-status', callback)
|
||||
}
|
||||
|
||||
// Public: Invoke the given callback when a multiple files' statuses have
|
||||
// changed. For example, on window focus, the status of all the paths in the
|
||||
// repo is checked. If any of them have changed, this will be fired. Call
|
||||
// {::getPathStatus} to get the status for your path of choice.
|
||||
//
|
||||
// * `callback` {Function}
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeStatuses (callback) {
|
||||
return this.emitter.on('did-change-statuses', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Repository Details
|
||||
*/
|
||||
|
||||
// Public: A {String} indicating the type of version control system used by
|
||||
// this repository.
|
||||
//
|
||||
// Returns `"git"`.
|
||||
getType () { return 'git' }
|
||||
|
||||
// Public: Returns the {String} path of the repository.
|
||||
getPath () {
|
||||
if (this.path == null) {
|
||||
this.path = fs.absolute(this.getRepo().getPath())
|
||||
}
|
||||
return this.path
|
||||
}
|
||||
|
||||
// Public: Returns the {String} working directory path of the repository.
|
||||
getWorkingDirectory () {
|
||||
return this.getRepo().getWorkingDirectory()
|
||||
}
|
||||
|
||||
// Public: Returns true if at the root, false if in a subfolder of the
|
||||
// repository.
|
||||
isProjectAtRoot () {
|
||||
if (this.projectAtRoot == null) {
|
||||
this.projectAtRoot = this.project && this.project.relativize(this.getWorkingDirectory()) === ''
|
||||
}
|
||||
return this.projectAtRoot
|
||||
}
|
||||
|
||||
// Public: Makes a path relative to the repository's working directory.
|
||||
relativize (path) {
|
||||
return this.getRepo().relativize(path)
|
||||
}
|
||||
|
||||
// Public: Returns true if the given branch exists.
|
||||
hasBranch (branch) {
|
||||
return this.getReferenceTarget(`refs/heads/${branch}`) != null
|
||||
}
|
||||
|
||||
// Public: Retrieves a shortened version of the HEAD reference value.
|
||||
//
|
||||
// This removes the leading segments of `refs/heads`, `refs/tags`, or
|
||||
// `refs/remotes`. It also shortens the SHA-1 of a detached `HEAD` to 7
|
||||
// characters.
|
||||
//
|
||||
// * `path` An optional {String} path in the repository to get this information
|
||||
// for, only needed if the repository contains submodules.
|
||||
//
|
||||
// Returns a {String}.
|
||||
getShortHead (path) {
|
||||
return this.getRepo(path).getShortHead()
|
||||
}
|
||||
|
||||
// Public: Is the given path a submodule in the repository?
|
||||
//
|
||||
// * `path` The {String} path to check.
|
||||
//
|
||||
// Returns a {Boolean}.
|
||||
isSubmodule (filePath) {
|
||||
if (!filePath) return false
|
||||
|
||||
const repo = this.getRepo(filePath)
|
||||
if (repo.isSubmodule(repo.relativize(filePath))) {
|
||||
return true
|
||||
} else {
|
||||
// Check if the filePath is a working directory in a repo that isn't the root.
|
||||
return repo !== this.getRepo() && repo.relativize(path.join(filePath, 'dir')) === 'dir'
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Returns the number of commits behind the current branch is from the
|
||||
// its upstream remote branch.
|
||||
//
|
||||
// * `reference` The {String} branch reference name.
|
||||
// * `path` The {String} path in the repository to get this information for,
|
||||
// only needed if the repository contains submodules.
|
||||
getAheadBehindCount (reference, path) {
|
||||
return this.getRepo(path).getAheadBehindCount(reference)
|
||||
}
|
||||
|
||||
// Public: Get the cached ahead/behind commit counts for the current branch's
|
||||
// upstream branch.
|
||||
//
|
||||
// * `path` An optional {String} path in the repository to get this information
|
||||
// for, only needed if the repository has submodules.
|
||||
//
|
||||
// Returns an {Object} with the following keys:
|
||||
// * `ahead` The {Number} of commits ahead.
|
||||
// * `behind` The {Number} of commits behind.
|
||||
getCachedUpstreamAheadBehindCount (path) {
|
||||
return this.getRepo(path).upstream || this.upstream
|
||||
}
|
||||
|
||||
// Public: Returns the git configuration value specified by the key.
|
||||
//
|
||||
// * `key` The {String} key for the configuration to lookup.
|
||||
// * `path` An optional {String} path in the repository to get this information
|
||||
// for, only needed if the repository has submodules.
|
||||
getConfigValue (key, path) {
|
||||
return this.getRepo(path).getConfigValue(key)
|
||||
}
|
||||
|
||||
// Public: Returns the origin url of the repository.
|
||||
//
|
||||
// * `path` (optional) {String} path in the repository to get this information
|
||||
// for, only needed if the repository has submodules.
|
||||
getOriginURL (path) {
|
||||
return this.getConfigValue('remote.origin.url', path)
|
||||
}
|
||||
|
||||
// Public: Returns the upstream branch for the current HEAD, or null if there
|
||||
// is no upstream branch for the current HEAD.
|
||||
//
|
||||
// * `path` An optional {String} path in the repo to get this information for,
|
||||
// only needed if the repository contains submodules.
|
||||
//
|
||||
// Returns a {String} branch name such as `refs/remotes/origin/master`.
|
||||
getUpstreamBranch (path) {
|
||||
return this.getRepo(path).getUpstreamBranch()
|
||||
}
|
||||
|
||||
// Public: Gets all the local and remote references.
|
||||
//
|
||||
// * `path` An optional {String} path in the repository to get this information
|
||||
// for, only needed if the repository has submodules.
|
||||
//
|
||||
// Returns an {Object} with the following keys:
|
||||
// * `heads` An {Array} of head reference names.
|
||||
// * `remotes` An {Array} of remote reference names.
|
||||
// * `tags` An {Array} of tag reference names.
|
||||
getReferences (path) {
|
||||
return this.getRepo(path).getReferences()
|
||||
}
|
||||
|
||||
// Public: Returns the current {String} SHA for the given reference.
|
||||
//
|
||||
// * `reference` The {String} reference to get the target of.
|
||||
// * `path` An optional {String} path in the repo to get the reference target
|
||||
// for. Only needed if the repository contains submodules.
|
||||
getReferenceTarget (reference, path) {
|
||||
return this.getRepo(path).getReferenceTarget(reference)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Reading Status
|
||||
*/
|
||||
|
||||
// Public: Returns true if the given path is modified.
|
||||
//
|
||||
// * `path` The {String} path to check.
|
||||
//
|
||||
// Returns a {Boolean} that's true if the `path` is modified.
|
||||
isPathModified (path) {
|
||||
return this.isStatusModified(this.getPathStatus(path))
|
||||
}
|
||||
|
||||
// Public: Returns true if the given path is new.
|
||||
//
|
||||
// * `path` The {String} path to check.
|
||||
//
|
||||
// Returns a {Boolean} that's true if the `path` is new.
|
||||
isPathNew (path) {
|
||||
return this.isStatusNew(this.getPathStatus(path))
|
||||
}
|
||||
|
||||
// Public: Is the given path ignored?
|
||||
//
|
||||
// * `path` The {String} path to check.
|
||||
//
|
||||
// Returns a {Boolean} that's true if the `path` is ignored.
|
||||
isPathIgnored (path) {
|
||||
return this.getRepo().isIgnored(this.relativize(path))
|
||||
}
|
||||
|
||||
// Public: Get the status of a directory in the repository's working directory.
|
||||
//
|
||||
// * `path` The {String} path to check.
|
||||
//
|
||||
// Returns a {Number} representing the status. This value can be passed to
|
||||
// {::isStatusModified} or {::isStatusNew} to get more information.
|
||||
getDirectoryStatus (directoryPath) {
|
||||
directoryPath = `${this.relativize(directoryPath)}/`
|
||||
let directoryStatus = 0
|
||||
for (let statusPath in this.statuses) {
|
||||
const status = this.statuses[statusPath]
|
||||
if (statusPath.startsWith(directoryPath)) directoryStatus |= status
|
||||
}
|
||||
return directoryStatus
|
||||
}
|
||||
|
||||
// Public: Get the status of a single path in the repository.
|
||||
//
|
||||
// * `path` A {String} repository-relative path.
|
||||
//
|
||||
// Returns a {Number} representing the status. This value can be passed to
|
||||
// {::isStatusModified} or {::isStatusNew} to get more information.
|
||||
getPathStatus (path) {
|
||||
const repo = this.getRepo(path)
|
||||
const relativePath = this.relativize(path)
|
||||
const currentPathStatus = this.statuses[relativePath] || 0
|
||||
let pathStatus = repo.getStatus(repo.relativize(path)) || 0
|
||||
if (repo.isStatusIgnored(pathStatus)) pathStatus = 0
|
||||
if (pathStatus > 0) {
|
||||
this.statuses[relativePath] = pathStatus
|
||||
} else {
|
||||
delete this.statuses[relativePath]
|
||||
}
|
||||
if (currentPathStatus !== pathStatus) {
|
||||
this.emitter.emit('did-change-status', {path, pathStatus})
|
||||
}
|
||||
|
||||
return pathStatus
|
||||
}
|
||||
|
||||
// Public: Get the cached status for the given path.
|
||||
//
|
||||
// * `path` A {String} path in the repository, relative or absolute.
|
||||
//
|
||||
// Returns a status {Number} or null if the path is not in the cache.
|
||||
getCachedPathStatus (path) {
|
||||
return this.statuses[this.relativize(path)]
|
||||
}
|
||||
|
||||
// Public: Returns true if the given status indicates modification.
|
||||
//
|
||||
// * `status` A {Number} representing the status.
|
||||
//
|
||||
// Returns a {Boolean} that's true if the `status` indicates modification.
|
||||
isStatusModified (status) { return this.getRepo().isStatusModified(status) }
|
||||
|
||||
// Public: Returns true if the given status indicates a new path.
|
||||
//
|
||||
// * `status` A {Number} representing the status.
|
||||
//
|
||||
// Returns a {Boolean} that's true if the `status` indicates a new path.
|
||||
isStatusNew (status) {
|
||||
return this.getRepo().isStatusNew(status)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Retrieving Diffs
|
||||
*/
|
||||
|
||||
// Public: Retrieves the number of lines added and removed to a path.
|
||||
//
|
||||
// This compares the working directory contents of the path to the `HEAD`
|
||||
// version.
|
||||
//
|
||||
// * `path` The {String} path to check.
|
||||
//
|
||||
// Returns an {Object} with the following keys:
|
||||
// * `added` The {Number} of added lines.
|
||||
// * `deleted` The {Number} of deleted lines.
|
||||
getDiffStats (path) {
|
||||
const repo = this.getRepo(path)
|
||||
return repo.getDiffStats(repo.relativize(path))
|
||||
}
|
||||
|
||||
// Public: Retrieves the line diffs comparing the `HEAD` version of the given
|
||||
// path and the given text.
|
||||
//
|
||||
// * `path` The {String} path relative to the repository.
|
||||
// * `text` The {String} to compare against the `HEAD` contents
|
||||
//
|
||||
// Returns an {Array} of hunk {Object}s with the following keys:
|
||||
// * `oldStart` The line {Number} of the old hunk.
|
||||
// * `newStart` The line {Number} of the new hunk.
|
||||
// * `oldLines` The {Number} of lines in the old hunk.
|
||||
// * `newLines` The {Number} of lines in the new hunk
|
||||
getLineDiffs (path, text) {
|
||||
// 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.
|
||||
const options = {ignoreEolWhitespace: process.platform === 'win32'}
|
||||
const repo = this.getRepo(path)
|
||||
return repo.getLineDiffs(repo.relativize(path), text, options)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Checking Out
|
||||
*/
|
||||
|
||||
// Public: Restore the contents of a path in the working directory and index
|
||||
// to the version at `HEAD`.
|
||||
//
|
||||
// This is essentially the same as running:
|
||||
//
|
||||
// ```sh
|
||||
// git reset HEAD -- <path>
|
||||
// git checkout HEAD -- <path>
|
||||
// ```
|
||||
//
|
||||
// * `path` The {String} path to checkout.
|
||||
//
|
||||
// Returns a {Boolean} that's true if the method was successful.
|
||||
checkoutHead (path) {
|
||||
const repo = this.getRepo(path)
|
||||
const headCheckedOut = repo.checkoutHead(repo.relativize(path))
|
||||
if (headCheckedOut) this.getPathStatus(path)
|
||||
return headCheckedOut
|
||||
}
|
||||
|
||||
// Public: Checks out a branch in your repository.
|
||||
//
|
||||
// * `reference` The {String} reference to checkout.
|
||||
// * `create` A {Boolean} value which, if true creates the new reference if
|
||||
// it doesn't exist.
|
||||
//
|
||||
// Returns a Boolean that's true if the method was successful.
|
||||
checkoutReference (reference, create) {
|
||||
return this.getRepo().checkoutReference(reference, create)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Private
|
||||
*/
|
||||
|
||||
// Subscribes to buffer events.
|
||||
subscribeToBuffer (buffer) {
|
||||
const getBufferPathStatus = () => {
|
||||
const bufferPath = buffer.getPath()
|
||||
if (bufferPath) this.getPathStatus(bufferPath)
|
||||
}
|
||||
|
||||
getBufferPathStatus()
|
||||
const bufferSubscriptions = new CompositeDisposable()
|
||||
bufferSubscriptions.add(buffer.onDidSave(getBufferPathStatus))
|
||||
bufferSubscriptions.add(buffer.onDidReload(getBufferPathStatus))
|
||||
bufferSubscriptions.add(buffer.onDidChangePath(getBufferPathStatus))
|
||||
bufferSubscriptions.add(buffer.onDidDestroy(() => {
|
||||
bufferSubscriptions.dispose()
|
||||
return this.subscriptions.remove(bufferSubscriptions)
|
||||
}))
|
||||
this.subscriptions.add(bufferSubscriptions)
|
||||
}
|
||||
|
||||
// Subscribes to editor view event.
|
||||
checkoutHeadForEditor (editor) {
|
||||
const buffer = editor.getBuffer()
|
||||
const bufferPath = buffer.getPath()
|
||||
if (bufferPath) {
|
||||
this.checkoutHead(bufferPath)
|
||||
return buffer.reload()
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the corresponding {Repository}
|
||||
getRepo (path) {
|
||||
if (this.repo) {
|
||||
return this.repo.submoduleForPath(path) || this.repo
|
||||
} else {
|
||||
throw new Error('Repository has been destroyed')
|
||||
}
|
||||
}
|
||||
|
||||
// Reread the index to update any values that have changed since the
|
||||
// last time the index was read.
|
||||
refreshIndex () {
|
||||
return this.getRepo().refreshIndex()
|
||||
}
|
||||
|
||||
// Refreshes the current git status in an outside process and asynchronously
|
||||
// updates the relevant properties.
|
||||
async refreshStatus () {
|
||||
const statusRefreshCount = ++this.statusRefreshCount
|
||||
const repo = this.getRepo()
|
||||
|
||||
const relativeProjectPaths = this.project && this.project.getPaths()
|
||||
.map(projectPath => this.relativize(projectPath))
|
||||
.filter(projectPath => (projectPath.length > 0) && !path.isAbsolute(projectPath))
|
||||
|
||||
const branch = await repo.getHeadAsync()
|
||||
const upstream = await repo.getAheadBehindCountAsync()
|
||||
|
||||
const statuses = {}
|
||||
const repoStatus = relativeProjectPaths.length > 0
|
||||
? await repo.getStatusAsync(relativeProjectPaths)
|
||||
: await repo.getStatusAsync()
|
||||
for (let filePath in repoStatus) {
|
||||
statuses[filePath] = repoStatus[filePath]
|
||||
}
|
||||
|
||||
const submodules = {}
|
||||
for (let submodulePath in repo.submodules) {
|
||||
const submoduleRepo = repo.submodules[submodulePath]
|
||||
submodules[submodulePath] = {
|
||||
branch: await submoduleRepo.getHeadAsync(),
|
||||
upstream: await submoduleRepo.getAheadBehindCountAsync()
|
||||
}
|
||||
|
||||
const workingDirectoryPath = submoduleRepo.getWorkingDirectory()
|
||||
const submoduleStatus = await submoduleRepo.getStatusAsync()
|
||||
for (let filePath in submoduleStatus) {
|
||||
const absolutePath = path.join(workingDirectoryPath, filePath)
|
||||
const relativizePath = repo.relativize(absolutePath)
|
||||
statuses[relativizePath] = submoduleStatus[filePath]
|
||||
}
|
||||
}
|
||||
|
||||
if (this.statusRefreshCount !== statusRefreshCount || this.isDestroyed()) return
|
||||
|
||||
const statusesUnchanged =
|
||||
_.isEqual(branch, this.branch) &&
|
||||
_.isEqual(statuses, this.statuses) &&
|
||||
_.isEqual(upstream, this.upstream) &&
|
||||
_.isEqual(submodules, this.submodules)
|
||||
|
||||
this.branch = branch
|
||||
this.statuses = statuses
|
||||
this.upstream = upstream
|
||||
this.submodules = submodules
|
||||
|
||||
for (let submodulePath in repo.submodules) {
|
||||
repo.submodules[submodulePath].upstream = submodules[submodulePath].upstream
|
||||
}
|
||||
|
||||
if (!statusesUnchanged) this.emitter.emit('did-change-statuses')
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
_ = require 'underscore-plus'
|
||||
{Emitter} = require 'event-kit'
|
||||
FirstMate = require 'first-mate'
|
||||
Token = require './token'
|
||||
fs = require 'fs-plus'
|
||||
|
||||
PathSplitRegex = new RegExp("[/.]")
|
||||
|
||||
# Extended: Syntax class holding the grammars used for tokenizing.
|
||||
#
|
||||
# An instance of this class is always available as the `atom.grammars` global.
|
||||
#
|
||||
# The Syntax class also contains properties for things such as the
|
||||
# language-specific comment regexes. See {::getProperty} for more details.
|
||||
module.exports =
|
||||
class GrammarRegistry extends FirstMate.GrammarRegistry
|
||||
constructor: ({@config}={}) ->
|
||||
super(maxTokensPerLine: 100)
|
||||
|
||||
createToken: (value, scopes) -> new Token({value, scopes})
|
||||
|
||||
# Extended: Select a grammar for the given file path and file contents.
|
||||
#
|
||||
# This picks the best match by checking the file path and contents against
|
||||
# each grammar.
|
||||
#
|
||||
# * `filePath` A {String} file path.
|
||||
# * `fileContents` A {String} of text for the file path.
|
||||
#
|
||||
# Returns a {Grammar}, never null.
|
||||
selectGrammar: (filePath, fileContents) ->
|
||||
bestMatch = null
|
||||
highestScore = -Infinity
|
||||
for grammar in @grammars
|
||||
score = @getGrammarScore(grammar, filePath, fileContents)
|
||||
if score > highestScore or not bestMatch?
|
||||
bestMatch = grammar
|
||||
highestScore = score
|
||||
bestMatch
|
||||
|
||||
# Extended: Returns a {Number} representing how well the grammar matches the
|
||||
# `filePath` and `contents`.
|
||||
getGrammarScore: (grammar, filePath, contents) ->
|
||||
return Infinity if @grammarOverrideForPath(filePath) is grammar.scopeName
|
||||
|
||||
contents = fs.readFileSync(filePath, 'utf8') if not contents? and fs.isFileSync(filePath)
|
||||
|
||||
score = @getGrammarPathScore(grammar, filePath)
|
||||
if score > 0 and not grammar.bundledPackage
|
||||
score += 0.25
|
||||
if @grammarMatchesContents(grammar, contents)
|
||||
score += 0.125
|
||||
score
|
||||
|
||||
getGrammarPathScore: (grammar, filePath) ->
|
||||
return -1 unless filePath
|
||||
filePath = filePath.replace(/\\/g, '/') if process.platform is 'win32'
|
||||
|
||||
pathComponents = filePath.toLowerCase().split(PathSplitRegex)
|
||||
pathScore = -1
|
||||
|
||||
fileTypes = grammar.fileTypes
|
||||
if customFileTypes = @config.get('core.customFileTypes')?[grammar.scopeName]
|
||||
fileTypes = fileTypes.concat(customFileTypes)
|
||||
|
||||
for fileType, i in fileTypes
|
||||
fileTypeComponents = fileType.toLowerCase().split(PathSplitRegex)
|
||||
pathSuffix = pathComponents[-fileTypeComponents.length..-1]
|
||||
if _.isEqual(pathSuffix, fileTypeComponents)
|
||||
pathScore = Math.max(pathScore, fileType.length)
|
||||
if i >= grammar.fileTypes.length
|
||||
pathScore += 0.5
|
||||
|
||||
pathScore
|
||||
|
||||
grammarMatchesContents: (grammar, contents) ->
|
||||
return false unless contents? and grammar.firstLineRegex?
|
||||
|
||||
escaped = false
|
||||
numberOfNewlinesInRegex = 0
|
||||
for character in grammar.firstLineRegex.source
|
||||
switch character
|
||||
when '\\'
|
||||
escaped = not escaped
|
||||
when 'n'
|
||||
numberOfNewlinesInRegex++ if escaped
|
||||
escaped = false
|
||||
else
|
||||
escaped = false
|
||||
lines = contents.split('\n')
|
||||
grammar.firstLineRegex.testSync(lines[0..numberOfNewlinesInRegex].join('\n'))
|
||||
|
||||
# Public: Get the grammar override for the given file path.
|
||||
#
|
||||
# * `filePath` A {String} file path.
|
||||
#
|
||||
# Returns a {Grammar} or undefined.
|
||||
grammarOverrideForPath: (filePath) ->
|
||||
@grammarOverridesByPath[filePath]
|
||||
|
||||
# Public: Set the grammar override for the given file path.
|
||||
#
|
||||
# * `filePath` A non-empty {String} file path.
|
||||
# * `scopeName` A {String} such as `"source.js"`.
|
||||
#
|
||||
# Returns a {Grammar} or undefined.
|
||||
setGrammarOverrideForPath: (filePath, scopeName) ->
|
||||
if filePath
|
||||
@grammarOverridesByPath[filePath] = scopeName
|
||||
|
||||
# Public: Remove the grammar override for the given file path.
|
||||
#
|
||||
# * `filePath` A {String} file path.
|
||||
#
|
||||
# Returns undefined.
|
||||
clearGrammarOverrideForPath: (filePath) ->
|
||||
delete @grammarOverridesByPath[filePath]
|
||||
undefined
|
||||
|
||||
# Public: Remove all grammar overrides.
|
||||
#
|
||||
# Returns undefined.
|
||||
clearGrammarOverrides: ->
|
||||
@grammarOverridesByPath = {}
|
||||
undefined
|
||||
585
src/grammar-registry.js
Normal file
585
src/grammar-registry.js
Normal file
@@ -0,0 +1,585 @@
|
||||
const _ = require('underscore-plus')
|
||||
const Grim = require('grim')
|
||||
const CSON = require('season')
|
||||
const FirstMate = require('first-mate')
|
||||
const {Disposable, CompositeDisposable} = require('event-kit')
|
||||
const TextMateLanguageMode = require('./text-mate-language-mode')
|
||||
const TreeSitterLanguageMode = require('./tree-sitter-language-mode')
|
||||
const TreeSitterGrammar = require('./tree-sitter-grammar')
|
||||
const Token = require('./token')
|
||||
const fs = require('fs-plus')
|
||||
const {Point, Range} = require('text-buffer')
|
||||
|
||||
const PATH_SPLIT_REGEX = new RegExp('[/.]')
|
||||
|
||||
// Extended: This class holds the grammars used for tokenizing.
|
||||
//
|
||||
// An instance of this class is always available as the `atom.grammars` global.
|
||||
module.exports =
|
||||
class GrammarRegistry {
|
||||
constructor ({config} = {}) {
|
||||
this.config = config
|
||||
this.subscriptions = new CompositeDisposable()
|
||||
this.textmateRegistry = new FirstMate.GrammarRegistry({maxTokensPerLine: 100, maxLineLength: 1000})
|
||||
this.clear()
|
||||
}
|
||||
|
||||
clear () {
|
||||
this.textmateRegistry.clear()
|
||||
this.treeSitterGrammarsById = {}
|
||||
if (this.subscriptions) this.subscriptions.dispose()
|
||||
this.subscriptions = new CompositeDisposable()
|
||||
this.languageOverridesByBufferId = new Map()
|
||||
this.grammarScoresByBuffer = new Map()
|
||||
this.textMateScopeNamesByTreeSitterLanguageId = new Map()
|
||||
this.treeSitterLanguageIdsByTextMateScopeName = new Map()
|
||||
|
||||
const grammarAddedOrUpdated = this.grammarAddedOrUpdated.bind(this)
|
||||
this.textmateRegistry.onDidAddGrammar(grammarAddedOrUpdated)
|
||||
this.textmateRegistry.onDidUpdateGrammar(grammarAddedOrUpdated)
|
||||
|
||||
this.subscriptions.add(this.config.onDidChange('core.useTreeSitterParsers', () => {
|
||||
this.grammarScoresByBuffer.forEach((score, buffer) => {
|
||||
if (!this.languageOverridesByBufferId.has(buffer.id)) {
|
||||
this.autoAssignLanguageMode(buffer)
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
serialize () {
|
||||
const languageOverridesByBufferId = {}
|
||||
this.languageOverridesByBufferId.forEach((languageId, bufferId) => {
|
||||
languageOverridesByBufferId[bufferId] = languageId
|
||||
})
|
||||
return {languageOverridesByBufferId}
|
||||
}
|
||||
|
||||
deserialize (params) {
|
||||
for (const bufferId in params.languageOverridesByBufferId || {}) {
|
||||
this.languageOverridesByBufferId.set(
|
||||
bufferId,
|
||||
params.languageOverridesByBufferId[bufferId]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
createToken (value, scopes) {
|
||||
return new Token({value, scopes})
|
||||
}
|
||||
|
||||
// Extended: set a {TextBuffer}'s language mode based on its path and content,
|
||||
// and continue to update its language mode as grammars are added or updated, or
|
||||
// the buffer's file path changes.
|
||||
//
|
||||
// * `buffer` The {TextBuffer} whose language mode will be maintained.
|
||||
//
|
||||
// Returns a {Disposable} that can be used to stop updating the buffer's
|
||||
// language mode.
|
||||
maintainLanguageMode (buffer) {
|
||||
this.grammarScoresByBuffer.set(buffer, null)
|
||||
|
||||
const languageOverride = this.languageOverridesByBufferId.get(buffer.id)
|
||||
if (languageOverride) {
|
||||
this.assignLanguageMode(buffer, languageOverride)
|
||||
} else {
|
||||
this.autoAssignLanguageMode(buffer)
|
||||
}
|
||||
|
||||
const pathChangeSubscription = buffer.onDidChangePath(() => {
|
||||
this.grammarScoresByBuffer.delete(buffer)
|
||||
if (!this.languageOverridesByBufferId.has(buffer.id)) {
|
||||
this.autoAssignLanguageMode(buffer)
|
||||
}
|
||||
})
|
||||
|
||||
const destroySubscription = buffer.onDidDestroy(() => {
|
||||
this.grammarScoresByBuffer.delete(buffer)
|
||||
this.languageOverridesByBufferId.delete(buffer.id)
|
||||
this.subscriptions.remove(destroySubscription)
|
||||
this.subscriptions.remove(pathChangeSubscription)
|
||||
})
|
||||
|
||||
this.subscriptions.add(pathChangeSubscription, destroySubscription)
|
||||
|
||||
return new Disposable(() => {
|
||||
destroySubscription.dispose()
|
||||
pathChangeSubscription.dispose()
|
||||
this.subscriptions.remove(pathChangeSubscription)
|
||||
this.subscriptions.remove(destroySubscription)
|
||||
this.grammarScoresByBuffer.delete(buffer)
|
||||
this.languageOverridesByBufferId.delete(buffer.id)
|
||||
})
|
||||
}
|
||||
|
||||
// Extended: Force a {TextBuffer} to use a different grammar than the
|
||||
// one that would otherwise be selected for it.
|
||||
//
|
||||
// * `buffer` The {TextBuffer} whose grammar will be set.
|
||||
// * `languageId` The {String} id of the desired language.
|
||||
//
|
||||
// Returns a {Boolean} that indicates whether the language was successfully
|
||||
// found.
|
||||
assignLanguageMode (buffer, languageId) {
|
||||
if (buffer.getBuffer) buffer = buffer.getBuffer()
|
||||
|
||||
let grammar = null
|
||||
if (languageId != null) {
|
||||
grammar = this.grammarForId(languageId)
|
||||
if (!grammar) return false
|
||||
this.languageOverridesByBufferId.set(buffer.id, languageId)
|
||||
} else {
|
||||
this.languageOverridesByBufferId.set(buffer.id, null)
|
||||
grammar = this.textmateRegistry.nullGrammar
|
||||
}
|
||||
|
||||
this.grammarScoresByBuffer.set(buffer, null)
|
||||
if (grammar !== buffer.getLanguageMode().grammar) {
|
||||
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Extended: Get the `languageId` that has been explicitly assigned to
|
||||
// to the given buffer, if any.
|
||||
//
|
||||
// Returns a {String} id of the language
|
||||
getAssignedLanguageId (buffer) {
|
||||
return this.languageOverridesByBufferId.get(buffer.id)
|
||||
}
|
||||
|
||||
// Extended: Remove any language mode override that has been set for the
|
||||
// given {TextBuffer}. This will assign to the buffer the best language
|
||||
// mode available.
|
||||
//
|
||||
// * `buffer` The {TextBuffer}.
|
||||
autoAssignLanguageMode (buffer) {
|
||||
const result = this.selectGrammarWithScore(
|
||||
buffer.getPath(),
|
||||
getGrammarSelectionContent(buffer)
|
||||
)
|
||||
this.languageOverridesByBufferId.delete(buffer.id)
|
||||
this.grammarScoresByBuffer.set(buffer, result.score)
|
||||
if (result.grammar !== buffer.getLanguageMode().grammar) {
|
||||
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(result.grammar, buffer))
|
||||
}
|
||||
}
|
||||
|
||||
languageModeForGrammarAndBuffer (grammar, buffer) {
|
||||
if (grammar instanceof TreeSitterGrammar) {
|
||||
return new TreeSitterLanguageMode({grammar, buffer, config: this.config, grammars: this})
|
||||
} else {
|
||||
return new TextMateLanguageMode({grammar, buffer, config: this.config})
|
||||
}
|
||||
}
|
||||
|
||||
// Extended: Select a grammar for the given file path and file contents.
|
||||
//
|
||||
// This picks the best match by checking the file path and contents against
|
||||
// each grammar.
|
||||
//
|
||||
// * `filePath` A {String} file path.
|
||||
// * `fileContents` A {String} of text for the file path.
|
||||
//
|
||||
// Returns a {Grammar}, never null.
|
||||
selectGrammar (filePath, fileContents) {
|
||||
return this.selectGrammarWithScore(filePath, fileContents).grammar
|
||||
}
|
||||
|
||||
selectGrammarWithScore (filePath, fileContents) {
|
||||
let bestMatch = null
|
||||
let highestScore = -Infinity
|
||||
this.forEachGrammar(grammar => {
|
||||
const score = this.getGrammarScore(grammar, filePath, fileContents)
|
||||
if (score > highestScore || bestMatch == null) {
|
||||
bestMatch = grammar
|
||||
highestScore = score
|
||||
}
|
||||
})
|
||||
return {grammar: bestMatch, score: highestScore}
|
||||
}
|
||||
|
||||
// Extended: Returns a {Number} representing how well the grammar matches the
|
||||
// `filePath` and `contents`.
|
||||
getGrammarScore (grammar, filePath, contents) {
|
||||
if (contents == null && fs.isFileSync(filePath)) {
|
||||
contents = fs.readFileSync(filePath, 'utf8')
|
||||
}
|
||||
|
||||
// Initially identify matching grammars based on the filename and the first
|
||||
// line of the file.
|
||||
let score = this.getGrammarPathScore(grammar, filePath)
|
||||
if (this.grammarMatchesPrefix(grammar, contents)) score += 0.5
|
||||
|
||||
// If multiple grammars match by one of the above criteria, break ties.
|
||||
if (score > 0) {
|
||||
// Prefer either TextMate or Tree-sitter grammars based on the user's settings.
|
||||
if (grammar instanceof TreeSitterGrammar) {
|
||||
if (this.config.get('core.useTreeSitterParsers')) {
|
||||
score += 0.1
|
||||
} else {
|
||||
return -Infinity
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer grammars with matching content regexes. Prefer a grammar with no content regex
|
||||
// over one with a non-matching content regex.
|
||||
if (grammar.contentRegex) {
|
||||
if (grammar.contentRegex.test(contents)) {
|
||||
score += 0.05
|
||||
} else {
|
||||
score -= 0.05
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer grammars that the user has manually installed over bundled grammars.
|
||||
if (!grammar.bundledPackage) score += 0.01
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
getGrammarPathScore (grammar, filePath) {
|
||||
if (!filePath) return -1
|
||||
if (process.platform === 'win32') { filePath = filePath.replace(/\\/g, '/') }
|
||||
|
||||
const pathComponents = filePath.toLowerCase().split(PATH_SPLIT_REGEX)
|
||||
let pathScore = 0
|
||||
|
||||
let customFileTypes
|
||||
if (this.config.get('core.customFileTypes')) {
|
||||
customFileTypes = this.config.get('core.customFileTypes')[grammar.scopeName]
|
||||
}
|
||||
|
||||
let { fileTypes } = grammar
|
||||
if (customFileTypes) {
|
||||
fileTypes = fileTypes.concat(customFileTypes)
|
||||
}
|
||||
|
||||
for (let i = 0; i < fileTypes.length; i++) {
|
||||
const fileType = fileTypes[i]
|
||||
const fileTypeComponents = fileType.toLowerCase().split(PATH_SPLIT_REGEX)
|
||||
const pathSuffix = pathComponents.slice(-fileTypeComponents.length)
|
||||
if (_.isEqual(pathSuffix, fileTypeComponents)) {
|
||||
pathScore = Math.max(pathScore, fileType.length)
|
||||
if (i >= grammar.fileTypes.length) {
|
||||
pathScore += 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pathScore
|
||||
}
|
||||
|
||||
grammarMatchesPrefix (grammar, contents) {
|
||||
if (contents && grammar.firstLineRegex) {
|
||||
let escaped = false
|
||||
let numberOfNewlinesInRegex = 0
|
||||
for (let character of grammar.firstLineRegex.source) {
|
||||
switch (character) {
|
||||
case '\\':
|
||||
escaped = !escaped
|
||||
break
|
||||
case 'n':
|
||||
if (escaped) { numberOfNewlinesInRegex++ }
|
||||
escaped = false
|
||||
break
|
||||
default:
|
||||
escaped = false
|
||||
}
|
||||
}
|
||||
|
||||
const prefix = contents.split('\n').slice(0, numberOfNewlinesInRegex + 1).join('\n')
|
||||
if (grammar.firstLineRegex.testSync) {
|
||||
return grammar.firstLineRegex.testSync(prefix)
|
||||
} else {
|
||||
return grammar.firstLineRegex.test(prefix)
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
forEachGrammar (callback) {
|
||||
this.textmateRegistry.grammars.forEach(callback)
|
||||
for (const grammarId in this.treeSitterGrammarsById) {
|
||||
const grammar = this.treeSitterGrammarsById[grammarId]
|
||||
if (grammar.scopeName) callback(grammar)
|
||||
}
|
||||
}
|
||||
|
||||
grammarForId (languageId) {
|
||||
if (!languageId) return null
|
||||
if (this.config.get('core.useTreeSitterParsers')) {
|
||||
return (
|
||||
this.treeSitterGrammarsById[languageId] ||
|
||||
this.textmateRegistry.grammarForScopeName(languageId)
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
this.textmateRegistry.grammarForScopeName(languageId) ||
|
||||
this.treeSitterGrammarsById[languageId]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: Get the grammar override for the given file path.
|
||||
//
|
||||
// * `filePath` A {String} file path.
|
||||
//
|
||||
// Returns a {String} such as `"source.js"`.
|
||||
grammarOverrideForPath (filePath) {
|
||||
Grim.deprecate('Use buffer.getLanguageMode().getLanguageId() instead')
|
||||
const buffer = atom.project.findBufferForPath(filePath)
|
||||
if (buffer) return this.getAssignedLanguageId(buffer)
|
||||
}
|
||||
|
||||
// Deprecated: Set the grammar override for the given file path.
|
||||
//
|
||||
// * `filePath` A non-empty {String} file path.
|
||||
// * `languageId` A {String} such as `"source.js"`.
|
||||
//
|
||||
// Returns undefined.
|
||||
setGrammarOverrideForPath (filePath, languageId) {
|
||||
Grim.deprecate('Use atom.grammars.assignLanguageMode(buffer, languageId) instead')
|
||||
const buffer = atom.project.findBufferForPath(filePath)
|
||||
if (buffer) {
|
||||
const grammar = this.grammarForScopeName(languageId)
|
||||
if (grammar) this.languageOverridesByBufferId.set(buffer.id, grammar.name)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the grammar override for the given file path.
|
||||
//
|
||||
// * `filePath` A {String} file path.
|
||||
//
|
||||
// Returns undefined.
|
||||
clearGrammarOverrideForPath (filePath) {
|
||||
Grim.deprecate('Use atom.grammars.autoAssignLanguageMode(buffer) instead')
|
||||
const buffer = atom.project.findBufferForPath(filePath)
|
||||
if (buffer) this.languageOverridesByBufferId.delete(buffer.id)
|
||||
}
|
||||
|
||||
grammarAddedOrUpdated (grammar) {
|
||||
if (grammar.scopeName && !grammar.id) grammar.id = grammar.scopeName
|
||||
|
||||
this.grammarScoresByBuffer.forEach((score, buffer) => {
|
||||
const languageMode = buffer.getLanguageMode()
|
||||
const languageOverride = this.languageOverridesByBufferId.get(buffer.id)
|
||||
|
||||
if (grammar === buffer.getLanguageMode().grammar ||
|
||||
grammar === this.grammarForId(languageOverride)) {
|
||||
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer))
|
||||
return
|
||||
} else if (!languageOverride) {
|
||||
const score = this.getGrammarScore(grammar, buffer.getPath(), getGrammarSelectionContent(buffer))
|
||||
const currentScore = this.grammarScoresByBuffer.get(buffer)
|
||||
if (currentScore == null || score > currentScore) {
|
||||
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer))
|
||||
this.grammarScoresByBuffer.set(buffer, score)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
languageMode.updateForInjection(grammar)
|
||||
})
|
||||
}
|
||||
|
||||
// Extended: Invoke the given callback when a grammar is added to the registry.
|
||||
//
|
||||
// * `callback` {Function} to call when a grammar is added.
|
||||
// * `grammar` {Grammar} that was added.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidAddGrammar (callback) {
|
||||
return this.textmateRegistry.onDidAddGrammar(callback)
|
||||
}
|
||||
|
||||
// Extended: Invoke the given callback when a grammar is updated due to a grammar
|
||||
// it depends on being added or removed from the registry.
|
||||
//
|
||||
// * `callback` {Function} to call when a grammar is updated.
|
||||
// * `grammar` {Grammar} that was updated.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidUpdateGrammar (callback) {
|
||||
return this.textmateRegistry.onDidUpdateGrammar(callback)
|
||||
}
|
||||
|
||||
// Experimental: Specify a type of syntax node that may embed other languages.
|
||||
//
|
||||
// * `grammarId` The {String} id of the parent language
|
||||
// * `injectionPoint` An {Object} with the following keys:
|
||||
// * `type` The {String} type of syntax node that may embed other languages
|
||||
// * `language` A {Function} that is called with syntax nodes of the specified `type` and
|
||||
// returns a {String} that will be tested against other grammars' `injectionRegex` in
|
||||
// order to determine what language should be embedded.
|
||||
// * `content` A {Function} that is called with syntax nodes of the specified `type` and
|
||||
// returns another syntax node or array of syntax nodes that contain the embedded source code.
|
||||
addInjectionPoint (grammarId, injectionPoint) {
|
||||
const grammar = this.treeSitterGrammarsById[grammarId]
|
||||
if (grammar) {
|
||||
grammar.injectionPoints.push(injectionPoint)
|
||||
} else {
|
||||
this.treeSitterGrammarsById[grammarId] = {
|
||||
injectionPoints: [injectionPoint]
|
||||
}
|
||||
}
|
||||
return new Disposable(() => {
|
||||
const grammar = this.treeSitterGrammarsById[grammarId]
|
||||
const index = grammar.injectionPoints.indexOf(injectionPoint)
|
||||
if (index !== -1) grammar.injectionPoints.splice(index, 1)
|
||||
})
|
||||
}
|
||||
|
||||
get nullGrammar () {
|
||||
return this.textmateRegistry.nullGrammar
|
||||
}
|
||||
|
||||
get grammars () {
|
||||
return this.textmateRegistry.grammars
|
||||
}
|
||||
|
||||
decodeTokens () {
|
||||
return this.textmateRegistry.decodeTokens.apply(this.textmateRegistry, arguments)
|
||||
}
|
||||
|
||||
grammarForScopeName (scopeName) {
|
||||
return this.grammarForId(scopeName)
|
||||
}
|
||||
|
||||
addGrammar (grammar) {
|
||||
if (grammar instanceof TreeSitterGrammar) {
|
||||
const existingParams = this.treeSitterGrammarsById[grammar.scopeName] || {}
|
||||
if (grammar.scopeName) this.treeSitterGrammarsById[grammar.scopeName] = grammar
|
||||
if (existingParams.injectionPoints) grammar.injectionPoints.push(...existingParams.injectionPoints)
|
||||
this.grammarAddedOrUpdated(grammar)
|
||||
return new Disposable(() => this.removeGrammar(grammar))
|
||||
} else {
|
||||
return this.textmateRegistry.addGrammar(grammar)
|
||||
}
|
||||
}
|
||||
|
||||
removeGrammar (grammar) {
|
||||
if (grammar instanceof TreeSitterGrammar) {
|
||||
delete this.treeSitterGrammarsById[grammar.scopeName]
|
||||
} else {
|
||||
return this.textmateRegistry.removeGrammar(grammar)
|
||||
}
|
||||
}
|
||||
|
||||
removeGrammarForScopeName (scopeName) {
|
||||
return this.textmateRegistry.removeGrammarForScopeName(scopeName)
|
||||
}
|
||||
|
||||
// Extended: Read a grammar asynchronously and add it to the registry.
|
||||
//
|
||||
// * `grammarPath` A {String} absolute file path to a grammar file.
|
||||
// * `callback` A {Function} to call when loaded with the following arguments:
|
||||
// * `error` An {Error}, may be null.
|
||||
// * `grammar` A {Grammar} or null if an error occured.
|
||||
loadGrammar (grammarPath, callback) {
|
||||
this.readGrammar(grammarPath, (error, grammar) => {
|
||||
if (error) return callback(error)
|
||||
this.addGrammar(grammar)
|
||||
callback(null, grammar)
|
||||
})
|
||||
}
|
||||
|
||||
// Extended: Read a grammar synchronously and add it to this registry.
|
||||
//
|
||||
// * `grammarPath` A {String} absolute file path to a grammar file.
|
||||
//
|
||||
// Returns a {Grammar}.
|
||||
loadGrammarSync (grammarPath) {
|
||||
const grammar = this.readGrammarSync(grammarPath)
|
||||
this.addGrammar(grammar)
|
||||
return grammar
|
||||
}
|
||||
|
||||
// Extended: Read a grammar asynchronously but don't add it to the registry.
|
||||
//
|
||||
// * `grammarPath` A {String} absolute file path to a grammar file.
|
||||
// * `callback` A {Function} to call when read with the following arguments:
|
||||
// * `error` An {Error}, may be null.
|
||||
// * `grammar` A {Grammar} or null if an error occured.
|
||||
//
|
||||
// Returns undefined.
|
||||
readGrammar (grammarPath, callback) {
|
||||
if (!callback) callback = () => {}
|
||||
CSON.readFile(grammarPath, (error, params = {}) => {
|
||||
if (error) return callback(error)
|
||||
try {
|
||||
callback(null, this.createGrammar(grammarPath, params))
|
||||
} catch (error) {
|
||||
callback(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Extended: Read a grammar synchronously but don't add it to the registry.
|
||||
//
|
||||
// * `grammarPath` A {String} absolute file path to a grammar file.
|
||||
//
|
||||
// Returns a {Grammar}.
|
||||
readGrammarSync (grammarPath) {
|
||||
return this.createGrammar(grammarPath, CSON.readFileSync(grammarPath) || {})
|
||||
}
|
||||
|
||||
createGrammar (grammarPath, params) {
|
||||
if (params.type === 'tree-sitter') {
|
||||
return new TreeSitterGrammar(this, grammarPath, params)
|
||||
} else {
|
||||
if (typeof params.scopeName !== 'string' || params.scopeName.length === 0) {
|
||||
throw new Error(`Grammar missing required scopeName property: ${grammarPath}`)
|
||||
}
|
||||
return this.textmateRegistry.createGrammar(grammarPath, params)
|
||||
}
|
||||
}
|
||||
|
||||
// Extended: Get all the grammars in this registry.
|
||||
//
|
||||
// Returns a non-empty {Array} of {Grammar} instances.
|
||||
getGrammars () {
|
||||
return this.textmateRegistry.getGrammars()
|
||||
}
|
||||
|
||||
scopeForId (id) {
|
||||
return this.textmateRegistry.scopeForId(id)
|
||||
}
|
||||
|
||||
treeSitterGrammarForLanguageString (languageString) {
|
||||
let longestMatchLength = 0
|
||||
let grammarWithLongestMatch = null
|
||||
for (const id in this.treeSitterGrammarsById) {
|
||||
const grammar = this.treeSitterGrammarsById[id]
|
||||
if (grammar.injectionRegex) {
|
||||
const match = languageString.match(grammar.injectionRegex)
|
||||
if (match) {
|
||||
const {length} = match[0]
|
||||
if (length > longestMatchLength) {
|
||||
grammarWithLongestMatch = grammar
|
||||
longestMatchLength = length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return grammarWithLongestMatch
|
||||
}
|
||||
|
||||
normalizeLanguageId (languageId) {
|
||||
if (this.config.get('core.useTreeSitterParsers')) {
|
||||
return this.treeSitterLanguageIdsByTextMateScopeName.get(languageId) || languageId
|
||||
} else {
|
||||
return this.textMateScopeNamesByTreeSitterLanguageId.get(languageId) || languageId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getGrammarSelectionContent (buffer) {
|
||||
return buffer.getTextInRange(Range(
|
||||
Point(0, 0),
|
||||
buffer.positionForCharacterIndex(1024)
|
||||
))
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
# Helper methods shared among GutterComponent classes.
|
||||
|
||||
module.exports =
|
||||
createGutterView: (gutterModel) ->
|
||||
domNode = document.createElement('div')
|
||||
domNode.classList.add('gutter')
|
||||
domNode.setAttribute('gutter-name', gutterModel.name)
|
||||
childNode = document.createElement('div')
|
||||
if gutterModel.name is 'line-number'
|
||||
childNode.classList.add('line-numbers')
|
||||
else
|
||||
childNode.classList.add('custom-decorations')
|
||||
domNode.appendChild(childNode)
|
||||
domNode
|
||||
|
||||
# Sets scrollHeight, scrollTop, and backgroundColor on the given domNode.
|
||||
setDimensionsAndBackground: (oldState, newState, domNode) ->
|
||||
if newState.scrollHeight isnt oldState.scrollHeight
|
||||
domNode.style.height = newState.scrollHeight + 'px'
|
||||
oldState.scrollHeight = newState.scrollHeight
|
||||
|
||||
if newState.scrollTop isnt oldState.scrollTop
|
||||
domNode.style['-webkit-transform'] = "translate3d(0px, #{-newState.scrollTop}px, 0px)"
|
||||
oldState.scrollTop = newState.scrollTop
|
||||
|
||||
if newState.backgroundColor isnt oldState.backgroundColor
|
||||
domNode.style.backgroundColor = newState.backgroundColor
|
||||
oldState.backgroundColor = newState.backgroundColor
|
||||
@@ -1,111 +0,0 @@
|
||||
_ = require 'underscore-plus'
|
||||
CustomGutterComponent = require './custom-gutter-component'
|
||||
LineNumberGutterComponent = require './line-number-gutter-component'
|
||||
|
||||
# The GutterContainerComponent manages the GutterComponents of a particular
|
||||
# TextEditorComponent.
|
||||
|
||||
module.exports =
|
||||
class GutterContainerComponent
|
||||
constructor: ({@onLineNumberGutterMouseDown, @editor, @domElementPool, @views}) ->
|
||||
# An array of objects of the form: {name: {String}, component: {Object}}
|
||||
@gutterComponents = []
|
||||
@gutterComponentsByGutterName = {}
|
||||
@lineNumberGutterComponent = null
|
||||
|
||||
@domNode = document.createElement('div')
|
||||
@domNode.classList.add('gutter-container')
|
||||
@domNode.style.display = 'flex'
|
||||
|
||||
destroy: ->
|
||||
for {name, component} in @gutterComponents
|
||||
component.destroy?()
|
||||
return
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
getLineNumberGutterComponent: ->
|
||||
@lineNumberGutterComponent
|
||||
|
||||
updateSync: (state) ->
|
||||
# The GutterContainerComponent expects the gutters to be sorted in the order
|
||||
# they should appear.
|
||||
newState = state.gutters
|
||||
|
||||
newGutterComponents = []
|
||||
newGutterComponentsByGutterName = {}
|
||||
for {gutter, visible, styles, content} in newState
|
||||
gutterComponent = @gutterComponentsByGutterName[gutter.name]
|
||||
if not gutterComponent
|
||||
if gutter.name is 'line-number'
|
||||
gutterComponent = new LineNumberGutterComponent({onMouseDown: @onLineNumberGutterMouseDown, @editor, gutter, @domElementPool, @views})
|
||||
@lineNumberGutterComponent = gutterComponent
|
||||
else
|
||||
gutterComponent = new CustomGutterComponent({gutter, @views})
|
||||
|
||||
if visible then gutterComponent.showNode() else gutterComponent.hideNode()
|
||||
# Pass the gutter only the state that it needs.
|
||||
if gutter.name is 'line-number'
|
||||
# For ease of use in the line number gutter component, set the shared
|
||||
# 'styles' as a field under the 'content'.
|
||||
gutterSubstate = _.clone(content)
|
||||
gutterSubstate.styles = styles
|
||||
else
|
||||
# Custom gutter 'content' is keyed on gutter name, so we cannot set
|
||||
# 'styles' as a subfield directly under it.
|
||||
gutterSubstate = {content, styles}
|
||||
gutterComponent.updateSync(gutterSubstate)
|
||||
|
||||
newGutterComponents.push({
|
||||
name: gutter.name,
|
||||
component: gutterComponent,
|
||||
})
|
||||
newGutterComponentsByGutterName[gutter.name] = gutterComponent
|
||||
|
||||
@reorderGutters(newGutterComponents, newGutterComponentsByGutterName)
|
||||
|
||||
@gutterComponents = newGutterComponents
|
||||
@gutterComponentsByGutterName = newGutterComponentsByGutterName
|
||||
|
||||
###
|
||||
Section: Private Methods
|
||||
###
|
||||
|
||||
reorderGutters: (newGutterComponents, newGutterComponentsByGutterName) ->
|
||||
# First, insert new gutters into the DOM.
|
||||
indexInOldGutters = 0
|
||||
oldGuttersLength = @gutterComponents.length
|
||||
|
||||
for gutterComponentDescription in newGutterComponents
|
||||
gutterComponent = gutterComponentDescription.component
|
||||
gutterName = gutterComponentDescription.name
|
||||
|
||||
if @gutterComponentsByGutterName[gutterName]
|
||||
# If the gutter existed previously, we first try to move the cursor to
|
||||
# the point at which it occurs in the previous gutters.
|
||||
matchingGutterFound = false
|
||||
while indexInOldGutters < oldGuttersLength
|
||||
existingGutterComponentDescription = @gutterComponents[indexInOldGutters]
|
||||
existingGutterComponent = existingGutterComponentDescription.component
|
||||
indexInOldGutters++
|
||||
if existingGutterComponent is gutterComponent
|
||||
matchingGutterFound = true
|
||||
break
|
||||
if not matchingGutterFound
|
||||
# If we've reached this point, the gutter previously existed, but its
|
||||
# position has moved. Remove it from the DOM and re-insert it.
|
||||
gutterComponent.getDomNode().remove()
|
||||
@domNode.appendChild(gutterComponent.getDomNode())
|
||||
|
||||
else
|
||||
if indexInOldGutters is oldGuttersLength
|
||||
@domNode.appendChild(gutterComponent.getDomNode())
|
||||
else
|
||||
@domNode.insertBefore(gutterComponent.getDomNode(), @domNode.children[indexInOldGutters])
|
||||
|
||||
# Remove any gutters that were not present in the new gutters state.
|
||||
for gutterComponentDescription in @gutterComponents
|
||||
if not newGutterComponentsByGutterName[gutterComponentDescription.name]
|
||||
gutterComponent = gutterComponentDescription.component
|
||||
gutterComponent.getDomNode().remove()
|
||||
@@ -1,82 +0,0 @@
|
||||
{Emitter} = require 'event-kit'
|
||||
Gutter = require './gutter'
|
||||
|
||||
module.exports =
|
||||
class GutterContainer
|
||||
constructor: (textEditor) ->
|
||||
@gutters = []
|
||||
@textEditor = textEditor
|
||||
@emitter = new Emitter
|
||||
|
||||
destroy: ->
|
||||
# Create a copy, because `Gutter::destroy` removes the gutter from
|
||||
# GutterContainer's @gutters.
|
||||
guttersToDestroy = @gutters.slice(0)
|
||||
for gutter in guttersToDestroy
|
||||
gutter.destroy() if gutter.name isnt 'line-number'
|
||||
@gutters = []
|
||||
@emitter.dispose()
|
||||
|
||||
addGutter: (options) ->
|
||||
options = options ? {}
|
||||
gutterName = options.name
|
||||
if gutterName is null
|
||||
throw new Error('A name is required to create a gutter.')
|
||||
if @gutterWithName(gutterName)
|
||||
throw new Error('Tried to create a gutter with a name that is already in use.')
|
||||
newGutter = new Gutter(this, options)
|
||||
|
||||
inserted = false
|
||||
# Insert the gutter into the gutters array, sorted in ascending order by 'priority'.
|
||||
# This could be optimized, but there are unlikely to be many gutters.
|
||||
for i in [0...@gutters.length]
|
||||
if @gutters[i].priority >= newGutter.priority
|
||||
@gutters.splice(i, 0, newGutter)
|
||||
inserted = true
|
||||
break
|
||||
if not inserted
|
||||
@gutters.push newGutter
|
||||
@emitter.emit 'did-add-gutter', newGutter
|
||||
return newGutter
|
||||
|
||||
getGutters: ->
|
||||
@gutters.slice()
|
||||
|
||||
gutterWithName: (name) ->
|
||||
for gutter in @gutters
|
||||
if gutter.name is name then return gutter
|
||||
null
|
||||
|
||||
observeGutters: (callback) ->
|
||||
callback(gutter) for gutter in @getGutters()
|
||||
@onDidAddGutter callback
|
||||
|
||||
onDidAddGutter: (callback) ->
|
||||
@emitter.on 'did-add-gutter', callback
|
||||
|
||||
onDidRemoveGutter: (callback) ->
|
||||
@emitter.on 'did-remove-gutter', callback
|
||||
|
||||
###
|
||||
Section: Private Methods
|
||||
###
|
||||
|
||||
# Processes the destruction of the gutter. Throws an error if this gutter is
|
||||
# not within this gutterContainer.
|
||||
removeGutter: (gutter) ->
|
||||
index = @gutters.indexOf(gutter)
|
||||
if index > -1
|
||||
@gutters.splice(index, 1)
|
||||
@emitter.emit 'did-remove-gutter', gutter.name
|
||||
else
|
||||
throw new Error 'The given gutter cannot be removed because it is not ' +
|
||||
'within this GutterContainer.'
|
||||
|
||||
# The public interface is Gutter::decorateMarker or TextEditor::decorateMarker.
|
||||
addGutterDecoration: (gutter, marker, options) ->
|
||||
if gutter.name is 'line-number'
|
||||
options.type = 'line-number'
|
||||
else
|
||||
options.type = 'gutter'
|
||||
options.gutterName = gutter.name
|
||||
@textEditor.decorateMarker(marker, options)
|
||||
108
src/gutter-container.js
Normal file
108
src/gutter-container.js
Normal file
@@ -0,0 +1,108 @@
|
||||
const {Emitter} = require('event-kit')
|
||||
const Gutter = require('./gutter')
|
||||
|
||||
module.exports = class GutterContainer {
|
||||
constructor (textEditor) {
|
||||
this.gutters = []
|
||||
this.textEditor = textEditor
|
||||
this.emitter = new Emitter()
|
||||
}
|
||||
|
||||
scheduleComponentUpdate () {
|
||||
this.textEditor.scheduleComponentUpdate()
|
||||
}
|
||||
|
||||
destroy () {
|
||||
// Create a copy, because `Gutter::destroy` removes the gutter from
|
||||
// GutterContainer's @gutters.
|
||||
const guttersToDestroy = this.gutters.slice(0)
|
||||
for (let gutter of guttersToDestroy) {
|
||||
if (gutter.name !== 'line-number') { gutter.destroy() }
|
||||
}
|
||||
this.gutters = []
|
||||
this.emitter.dispose()
|
||||
}
|
||||
|
||||
addGutter (options) {
|
||||
options = options || {}
|
||||
const gutterName = options.name
|
||||
if (gutterName === null) {
|
||||
throw new Error('A name is required to create a gutter.')
|
||||
}
|
||||
if (this.gutterWithName(gutterName)) {
|
||||
throw new Error('Tried to create a gutter with a name that is already in use.')
|
||||
}
|
||||
const newGutter = new Gutter(this, options)
|
||||
|
||||
let inserted = false
|
||||
// Insert the gutter into the gutters array, sorted in ascending order by 'priority'.
|
||||
// This could be optimized, but there are unlikely to be many gutters.
|
||||
for (let i = 0; i < this.gutters.length; i++) {
|
||||
if (this.gutters[i].priority >= newGutter.priority) {
|
||||
this.gutters.splice(i, 0, newGutter)
|
||||
inserted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!inserted) {
|
||||
this.gutters.push(newGutter)
|
||||
}
|
||||
this.scheduleComponentUpdate()
|
||||
this.emitter.emit('did-add-gutter', newGutter)
|
||||
return newGutter
|
||||
}
|
||||
|
||||
getGutters () {
|
||||
return this.gutters.slice()
|
||||
}
|
||||
|
||||
gutterWithName (name) {
|
||||
for (let gutter of this.gutters) {
|
||||
if (gutter.name === name) { return gutter }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
observeGutters (callback) {
|
||||
for (let gutter of this.getGutters()) { callback(gutter) }
|
||||
return this.onDidAddGutter(callback)
|
||||
}
|
||||
|
||||
onDidAddGutter (callback) {
|
||||
return this.emitter.on('did-add-gutter', callback)
|
||||
}
|
||||
|
||||
onDidRemoveGutter (callback) {
|
||||
return this.emitter.on('did-remove-gutter', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Private Methods
|
||||
*/
|
||||
|
||||
// Processes the destruction of the gutter. Throws an error if this gutter is
|
||||
// not within this gutterContainer.
|
||||
removeGutter (gutter) {
|
||||
const index = this.gutters.indexOf(gutter)
|
||||
if (index > -1) {
|
||||
this.gutters.splice(index, 1)
|
||||
this.scheduleComponentUpdate()
|
||||
this.emitter.emit('did-remove-gutter', gutter.name)
|
||||
} else {
|
||||
throw new Error('The given gutter cannot be removed because it is not ' +
|
||||
'within this GutterContainer.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// The public interface is Gutter::decorateMarker or TextEditor::decorateMarker.
|
||||
addGutterDecoration (gutter, marker, options) {
|
||||
if (gutter.type === 'line-number') {
|
||||
options.type = 'line-number'
|
||||
} else {
|
||||
options.type = 'gutter'
|
||||
}
|
||||
options.gutterName = gutter.name
|
||||
return this.textEditor.decorateMarker(marker, options)
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
{Emitter} = require 'event-kit'
|
||||
|
||||
DefaultPriority = -100
|
||||
|
||||
# Extended: Represents a gutter within a {TextEditor}.
|
||||
#
|
||||
# See {TextEditor::addGutter} for information on creating a gutter.
|
||||
module.exports =
|
||||
class Gutter
|
||||
constructor: (gutterContainer, options) ->
|
||||
@gutterContainer = gutterContainer
|
||||
@name = options?.name
|
||||
@priority = options?.priority ? DefaultPriority
|
||||
@visible = options?.visible ? true
|
||||
|
||||
@emitter = new Emitter
|
||||
|
||||
###
|
||||
Section: Gutter Destruction
|
||||
###
|
||||
|
||||
# Essential: Destroys the gutter.
|
||||
destroy: ->
|
||||
if @name is 'line-number'
|
||||
throw new Error('The line-number gutter cannot be destroyed.')
|
||||
else
|
||||
@gutterContainer.removeGutter(this)
|
||||
@emitter.emit 'did-destroy'
|
||||
@emitter.dispose()
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
###
|
||||
|
||||
# Essential: Calls your `callback` when the gutter's visibility changes.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
# * `gutter` The gutter whose visibility changed.
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeVisible: (callback) ->
|
||||
@emitter.on 'did-change-visible', callback
|
||||
|
||||
# Essential: Calls your `callback` when the gutter is destroyed.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy: (callback) ->
|
||||
@emitter.on 'did-destroy', callback
|
||||
|
||||
###
|
||||
Section: Visibility
|
||||
###
|
||||
|
||||
# Essential: Hide the gutter.
|
||||
hide: ->
|
||||
if @visible
|
||||
@visible = false
|
||||
@emitter.emit 'did-change-visible', this
|
||||
|
||||
# Essential: Show the gutter.
|
||||
show: ->
|
||||
if not @visible
|
||||
@visible = true
|
||||
@emitter.emit 'did-change-visible', this
|
||||
|
||||
# Essential: Determine whether the gutter is visible.
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
isVisible: ->
|
||||
@visible
|
||||
|
||||
# Essential: Add a decoration that tracks a {TextEditorMarker}. When the marker moves,
|
||||
# is invalidated, or is destroyed, the decoration will be updated to reflect
|
||||
# the marker's state.
|
||||
#
|
||||
# ## Arguments
|
||||
#
|
||||
# * `marker` A {TextEditorMarker} you want this decoration to follow.
|
||||
# * `decorationParams` An {Object} representing the decoration. It is passed
|
||||
# to {TextEditor::decorateMarker} as its `decorationParams` and so supports
|
||||
# all options documented there.
|
||||
# * `type` __Caveat__: set to `'line-number'` if this is the line-number
|
||||
# gutter, `'gutter'` otherwise. This cannot be overridden.
|
||||
#
|
||||
# Returns a {Decoration} object
|
||||
decorateMarker: (marker, options) ->
|
||||
@gutterContainer.addGutterDecoration(this, marker, options)
|
||||
113
src/gutter.js
Normal file
113
src/gutter.js
Normal file
@@ -0,0 +1,113 @@
|
||||
const {Emitter} = require('event-kit')
|
||||
|
||||
const DefaultPriority = -100
|
||||
|
||||
// Extended: Represents a gutter within a {TextEditor}.
|
||||
//
|
||||
// See {TextEditor::addGutter} for information on creating a gutter.
|
||||
module.exports = class Gutter {
|
||||
constructor (gutterContainer, options) {
|
||||
this.gutterContainer = gutterContainer
|
||||
this.name = options && options.name
|
||||
this.priority = (options && options.priority != null) ? options.priority : DefaultPriority
|
||||
this.visible = (options && options.visible != null) ? options.visible : true
|
||||
this.type = (options && options.type != null) ? options.type : 'decorated'
|
||||
this.labelFn = options && options.labelFn
|
||||
this.className = options && options.class
|
||||
|
||||
this.onMouseDown = options && options.onMouseDown
|
||||
this.onMouseMove = options && options.onMouseMove
|
||||
|
||||
this.emitter = new Emitter()
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Gutter Destruction
|
||||
*/
|
||||
|
||||
// Essential: Destroys the gutter.
|
||||
destroy () {
|
||||
if (this.name === 'line-number') {
|
||||
throw new Error('The line-number gutter cannot be destroyed.')
|
||||
} else {
|
||||
this.gutterContainer.removeGutter(this)
|
||||
this.emitter.emit('did-destroy')
|
||||
this.emitter.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Event Subscription
|
||||
*/
|
||||
|
||||
// Essential: Calls your `callback` when the gutter's visibility changes.
|
||||
//
|
||||
// * `callback` {Function}
|
||||
// * `gutter` The gutter whose visibility changed.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeVisible (callback) {
|
||||
return this.emitter.on('did-change-visible', callback)
|
||||
}
|
||||
|
||||
// Essential: Calls your `callback` when the gutter is destroyed.
|
||||
//
|
||||
// * `callback` {Function}
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy (callback) {
|
||||
return this.emitter.once('did-destroy', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Visibility
|
||||
*/
|
||||
|
||||
// Essential: Hide the gutter.
|
||||
hide () {
|
||||
if (this.visible) {
|
||||
this.visible = false
|
||||
this.gutterContainer.scheduleComponentUpdate()
|
||||
this.emitter.emit('did-change-visible', this)
|
||||
}
|
||||
}
|
||||
|
||||
// Essential: Show the gutter.
|
||||
show () {
|
||||
if (!this.visible) {
|
||||
this.visible = true
|
||||
this.gutterContainer.scheduleComponentUpdate()
|
||||
this.emitter.emit('did-change-visible', this)
|
||||
}
|
||||
}
|
||||
|
||||
// Essential: Determine whether the gutter is visible.
|
||||
//
|
||||
// Returns a {Boolean}.
|
||||
isVisible () {
|
||||
return this.visible
|
||||
}
|
||||
|
||||
// Essential: Add a decoration that tracks a {DisplayMarker}. When the marker moves,
|
||||
// is invalidated, or is destroyed, the decoration will be updated to reflect
|
||||
// the marker's state.
|
||||
//
|
||||
// ## Arguments
|
||||
//
|
||||
// * `marker` A {DisplayMarker} you want this decoration to follow.
|
||||
// * `decorationParams` An {Object} representing the decoration. It is passed
|
||||
// to {TextEditor::decorateMarker} as its `decorationParams` and so supports
|
||||
// all options documented there.
|
||||
// * `type` __Caveat__: set to `'line-number'` if this is the line-number
|
||||
// gutter, `'gutter'` otherwise. This cannot be overridden.
|
||||
//
|
||||
// Returns a {Decoration} object
|
||||
decorateMarker (marker, options) {
|
||||
return this.gutterContainer.addGutterDecoration(this, marker, options)
|
||||
}
|
||||
|
||||
getElement () {
|
||||
if (this.element == null) this.element = document.createElement('div')
|
||||
return this.element
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
RegionStyleProperties = ['top', 'left', 'right', 'width', 'height']
|
||||
SpaceRegex = /\s+/
|
||||
|
||||
module.exports =
|
||||
class HighlightsComponent
|
||||
oldState: null
|
||||
|
||||
constructor: (@domElementPool) ->
|
||||
@highlightNodesById = {}
|
||||
@regionNodesByHighlightId = {}
|
||||
|
||||
@domNode = @domElementPool.buildElement("div", "highlights")
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
updateSync: (state) ->
|
||||
newState = state.highlights
|
||||
@oldState ?= {}
|
||||
|
||||
# remove highlights
|
||||
for id of @oldState
|
||||
unless newState[id]?
|
||||
@domElementPool.freeElementAndDescendants(@highlightNodesById[id])
|
||||
delete @highlightNodesById[id]
|
||||
delete @regionNodesByHighlightId[id]
|
||||
delete @oldState[id]
|
||||
|
||||
# add or update highlights
|
||||
for id, highlightState of newState
|
||||
unless @oldState[id]?
|
||||
highlightNode = @domElementPool.buildElement("div", "highlight")
|
||||
@highlightNodesById[id] = highlightNode
|
||||
@regionNodesByHighlightId[id] = {}
|
||||
@domNode.appendChild(highlightNode)
|
||||
@updateHighlightNode(id, highlightState)
|
||||
|
||||
return
|
||||
|
||||
updateHighlightNode: (id, newHighlightState) ->
|
||||
highlightNode = @highlightNodesById[id]
|
||||
oldHighlightState = (@oldState[id] ?= {regions: [], flashCount: 0})
|
||||
|
||||
# update class
|
||||
if newHighlightState.class isnt oldHighlightState.class
|
||||
if oldHighlightState.class?
|
||||
if SpaceRegex.test(oldHighlightState.class)
|
||||
highlightNode.classList.remove(oldHighlightState.class.split(SpaceRegex)...)
|
||||
else
|
||||
highlightNode.classList.remove(oldHighlightState.class)
|
||||
|
||||
if SpaceRegex.test(newHighlightState.class)
|
||||
highlightNode.classList.add(newHighlightState.class.split(SpaceRegex)...)
|
||||
else
|
||||
highlightNode.classList.add(newHighlightState.class)
|
||||
|
||||
oldHighlightState.class = newHighlightState.class
|
||||
|
||||
@updateHighlightRegions(id, newHighlightState)
|
||||
@flashHighlightNodeIfRequested(id, newHighlightState)
|
||||
|
||||
updateHighlightRegions: (id, newHighlightState) ->
|
||||
oldHighlightState = @oldState[id]
|
||||
highlightNode = @highlightNodesById[id]
|
||||
|
||||
# remove regions
|
||||
while oldHighlightState.regions.length > newHighlightState.regions.length
|
||||
oldHighlightState.regions.pop()
|
||||
@domElementPool.freeElementAndDescendants(@regionNodesByHighlightId[id][oldHighlightState.regions.length])
|
||||
delete @regionNodesByHighlightId[id][oldHighlightState.regions.length]
|
||||
|
||||
# add or update regions
|
||||
for newRegionState, i in newHighlightState.regions
|
||||
unless oldHighlightState.regions[i]?
|
||||
oldHighlightState.regions[i] = {}
|
||||
regionNode = @domElementPool.buildElement("div", "region")
|
||||
# This prevents highlights at the tiles boundaries to be hidden by the
|
||||
# subsequent tile. When this happens, subpixel anti-aliasing gets
|
||||
# disabled.
|
||||
regionNode.style.boxSizing = "border-box"
|
||||
regionNode.classList.add(newHighlightState.deprecatedRegionClass) if newHighlightState.deprecatedRegionClass?
|
||||
@regionNodesByHighlightId[id][i] = regionNode
|
||||
highlightNode.appendChild(regionNode)
|
||||
|
||||
oldRegionState = oldHighlightState.regions[i]
|
||||
regionNode = @regionNodesByHighlightId[id][i]
|
||||
|
||||
for property in RegionStyleProperties
|
||||
if newRegionState[property] isnt oldRegionState[property]
|
||||
oldRegionState[property] = newRegionState[property]
|
||||
if newRegionState[property]?
|
||||
regionNode.style[property] = newRegionState[property] + 'px'
|
||||
else
|
||||
regionNode.style[property] = ''
|
||||
|
||||
return
|
||||
|
||||
flashHighlightNodeIfRequested: (id, newHighlightState) ->
|
||||
oldHighlightState = @oldState[id]
|
||||
return unless newHighlightState.flashCount > oldHighlightState.flashCount
|
||||
|
||||
highlightNode = @highlightNodesById[id]
|
||||
|
||||
addFlashClass = =>
|
||||
highlightNode.classList.add(newHighlightState.flashClass)
|
||||
oldHighlightState.flashClass = newHighlightState.flashClass
|
||||
@flashTimeoutId = setTimeout(removeFlashClass, newHighlightState.flashDuration)
|
||||
|
||||
removeFlashClass = =>
|
||||
highlightNode.classList.remove(oldHighlightState.flashClass)
|
||||
oldHighlightState.flashClass = null
|
||||
clearTimeout(@flashTimeoutId)
|
||||
|
||||
if oldHighlightState.flashClass?
|
||||
removeFlashClass()
|
||||
requestAnimationFrame(addFlashClass)
|
||||
else
|
||||
addFlashClass()
|
||||
|
||||
oldHighlightState.flashCount = newHighlightState.flashCount
|
||||
130
src/history-manager.js
Normal file
130
src/history-manager.js
Normal file
@@ -0,0 +1,130 @@
|
||||
const {Emitter, CompositeDisposable} = require('event-kit')
|
||||
|
||||
// Extended: History manager for remembering which projects have been opened.
|
||||
//
|
||||
// An instance of this class is always available as the `atom.history` global.
|
||||
//
|
||||
// The project history is used to enable the 'Reopen Project' menu.
|
||||
class HistoryManager {
|
||||
constructor ({project, commands, stateStore}) {
|
||||
this.stateStore = stateStore
|
||||
this.emitter = new Emitter()
|
||||
this.projects = []
|
||||
this.disposables = new CompositeDisposable()
|
||||
this.disposables.add(commands.add('atom-workspace', {'application:clear-project-history': this.clearProjects.bind(this)}, false))
|
||||
this.disposables.add(project.onDidChangePaths((projectPaths) => this.addProject(projectPaths)))
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.disposables.dispose()
|
||||
}
|
||||
|
||||
// Public: Obtain a list of previously opened projects.
|
||||
//
|
||||
// Returns an {Array} of {HistoryProject} objects, most recent first.
|
||||
getProjects () {
|
||||
return this.projects.map(p => new HistoryProject(p.paths, p.lastOpened))
|
||||
}
|
||||
|
||||
// Public: Clear all projects from the history.
|
||||
//
|
||||
// Note: This is not a privacy function - other traces will still exist,
|
||||
// e.g. window state.
|
||||
//
|
||||
// Return a {Promise} that resolves when the history has been successfully
|
||||
// cleared.
|
||||
async clearProjects () {
|
||||
this.projects = []
|
||||
await this.saveState()
|
||||
this.didChangeProjects()
|
||||
}
|
||||
|
||||
// Public: Invoke the given callback when the list of projects changes.
|
||||
//
|
||||
// * `callback` {Function}
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeProjects (callback) {
|
||||
return this.emitter.on('did-change-projects', callback)
|
||||
}
|
||||
|
||||
didChangeProjects (args = {reloaded: false}) {
|
||||
this.emitter.emit('did-change-projects', args)
|
||||
}
|
||||
|
||||
async addProject (paths, lastOpened) {
|
||||
if (paths.length === 0) return
|
||||
|
||||
let project = this.getProject(paths)
|
||||
if (!project) {
|
||||
project = new HistoryProject(paths)
|
||||
this.projects.push(project)
|
||||
}
|
||||
project.lastOpened = lastOpened || new Date()
|
||||
this.projects.sort((a, b) => b.lastOpened - a.lastOpened)
|
||||
|
||||
await this.saveState()
|
||||
this.didChangeProjects()
|
||||
}
|
||||
|
||||
async removeProject (paths) {
|
||||
if (paths.length === 0) return
|
||||
|
||||
let project = this.getProject(paths)
|
||||
if (!project) return
|
||||
|
||||
let index = this.projects.indexOf(project)
|
||||
this.projects.splice(index, 1)
|
||||
|
||||
await this.saveState()
|
||||
this.didChangeProjects()
|
||||
}
|
||||
|
||||
getProject (paths) {
|
||||
for (var i = 0; i < this.projects.length; i++) {
|
||||
if (arrayEquivalent(paths, this.projects[i].paths)) {
|
||||
return this.projects[i]
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async loadState () {
|
||||
const history = await this.stateStore.load('history-manager')
|
||||
if (history && history.projects) {
|
||||
this.projects = history.projects.filter(p => Array.isArray(p.paths) && p.paths.length > 0).map(p => new HistoryProject(p.paths, new Date(p.lastOpened)))
|
||||
this.didChangeProjects({reloaded: true})
|
||||
} else {
|
||||
this.projects = []
|
||||
}
|
||||
}
|
||||
|
||||
async saveState () {
|
||||
const projects = this.projects.map(p => ({paths: p.paths, lastOpened: p.lastOpened}))
|
||||
await this.stateStore.save('history-manager', {projects})
|
||||
}
|
||||
}
|
||||
|
||||
function arrayEquivalent (a, b) {
|
||||
if (a.length !== b.length) return false
|
||||
for (var i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
class HistoryProject {
|
||||
constructor (paths, lastOpened) {
|
||||
this.paths = paths
|
||||
this.lastOpened = lastOpened || new Date()
|
||||
}
|
||||
|
||||
set paths (paths) { this._paths = paths }
|
||||
get paths () { return this._paths }
|
||||
|
||||
set lastOpened (lastOpened) { this._lastOpened = lastOpened }
|
||||
get lastOpened () { return this._lastOpened }
|
||||
}
|
||||
|
||||
module.exports = {HistoryManager, HistoryProject}
|
||||
@@ -1,10 +1,87 @@
|
||||
AtomEnvironment = require './atom-environment'
|
||||
ApplicationDelegate = require './application-delegate'
|
||||
Clipboard = require './clipboard'
|
||||
TextEditor = require './text-editor'
|
||||
TextEditorComponent = require './text-editor-component'
|
||||
FileSystemBlobStore = require './file-system-blob-store'
|
||||
NativeCompileCache = require './native-compile-cache'
|
||||
CompileCache = require './compile-cache'
|
||||
ModuleCache = require './module-cache'
|
||||
|
||||
if global.isGeneratingSnapshot
|
||||
require('about')
|
||||
require('archive-view')
|
||||
require('autocomplete-atom-api')
|
||||
require('autocomplete-css')
|
||||
require('autocomplete-html')
|
||||
require('autocomplete-plus')
|
||||
require('autocomplete-snippets')
|
||||
require('autoflow')
|
||||
require('autosave')
|
||||
require('background-tips')
|
||||
require('bookmarks')
|
||||
require('bracket-matcher')
|
||||
require('command-palette')
|
||||
require('deprecation-cop')
|
||||
require('dev-live-reload')
|
||||
require('encoding-selector')
|
||||
require('exception-reporting')
|
||||
require('dalek')
|
||||
require('find-and-replace')
|
||||
require('fuzzy-finder')
|
||||
require('github')
|
||||
require('git-diff')
|
||||
require('go-to-line')
|
||||
require('grammar-selector')
|
||||
require('image-view')
|
||||
require('incompatible-packages')
|
||||
require('keybinding-resolver')
|
||||
require('language-html')
|
||||
require('language-javascript')
|
||||
require('language-ruby')
|
||||
require('line-ending-selector')
|
||||
require('link')
|
||||
require('markdown-preview')
|
||||
require('metrics')
|
||||
require('notifications')
|
||||
require('open-on-github')
|
||||
require('package-generator')
|
||||
require('settings-view')
|
||||
require('snippets')
|
||||
require('spell-check')
|
||||
require('status-bar')
|
||||
require('styleguide')
|
||||
require('symbols-view')
|
||||
require('tabs')
|
||||
require('timecop')
|
||||
require('tree-view')
|
||||
require('update-package-dependencies')
|
||||
require('welcome')
|
||||
require('whitespace')
|
||||
require('wrap-guide')
|
||||
|
||||
clipboard = new Clipboard
|
||||
TextEditor.setClipboard(clipboard)
|
||||
TextEditor.viewForItem = (item) -> atom.views.getView(item)
|
||||
|
||||
global.atom = new AtomEnvironment({
|
||||
clipboard,
|
||||
applicationDelegate: new ApplicationDelegate,
|
||||
enablePersistence: true
|
||||
})
|
||||
|
||||
TextEditor.setScheduler(global.atom.views)
|
||||
global.atom.preloadPackages()
|
||||
|
||||
# Like sands through the hourglass, so are the days of our lives.
|
||||
module.exports = ({blobStore}) ->
|
||||
{updateProcessEnv} = require('./update-process-env')
|
||||
path = require 'path'
|
||||
require './window'
|
||||
{getWindowLoadSettings} = require './window-load-settings-helpers'
|
||||
|
||||
{resourcePath, isSpec, devMode, env} = getWindowLoadSettings()
|
||||
getWindowLoadSettings = require './get-window-load-settings'
|
||||
{ipcRenderer} = require 'electron'
|
||||
{resourcePath, devMode, env} = getWindowLoadSettings()
|
||||
require './electron-shims'
|
||||
|
||||
# Add application-specific exports to module search path.
|
||||
exportsPath = path.join(resourcePath, 'exports')
|
||||
@@ -14,20 +91,18 @@ module.exports = ({blobStore}) ->
|
||||
# Make React faster
|
||||
process.env.NODE_ENV ?= 'production' unless devMode
|
||||
|
||||
AtomEnvironment = require './atom-environment'
|
||||
ApplicationDelegate = require './application-delegate'
|
||||
window.atom = new AtomEnvironment({
|
||||
global.atom.initialize({
|
||||
window, document, blobStore,
|
||||
applicationDelegate: new ApplicationDelegate,
|
||||
configDirPath: process.env.ATOM_HOME
|
||||
enablePersistence: true
|
||||
env: env
|
||||
configDirPath: process.env.ATOM_HOME,
|
||||
env: process.env
|
||||
})
|
||||
|
||||
atom.startEditorWindow().then ->
|
||||
|
||||
global.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)
|
||||
ipcRenderer.on('environment', (event, env) ->
|
||||
updateProcessEnv(env)
|
||||
)
|
||||
|
||||
113
src/initialize-benchmark-window.js
Normal file
113
src/initialize-benchmark-window.js
Normal file
@@ -0,0 +1,113 @@
|
||||
const {remote} = require('electron')
|
||||
const path = require('path')
|
||||
const ipcHelpers = require('./ipc-helpers')
|
||||
const util = require('util')
|
||||
|
||||
module.exports = async function () {
|
||||
const getWindowLoadSettings = require('./get-window-load-settings')
|
||||
const {test, headless, resourcePath, benchmarkPaths} = getWindowLoadSettings()
|
||||
try {
|
||||
const Clipboard = require('../src/clipboard')
|
||||
const ApplicationDelegate = require('../src/application-delegate')
|
||||
const AtomEnvironment = require('../src/atom-environment')
|
||||
const TextEditor = require('../src/text-editor')
|
||||
require('./electron-shims')
|
||||
|
||||
const exportsPath = path.join(resourcePath, 'exports')
|
||||
require('module').globalPaths.push(exportsPath) // Add 'exports' to module search path.
|
||||
process.env.NODE_PATH = exportsPath // Set NODE_PATH env variable since tasks may need it.
|
||||
|
||||
document.title = 'Benchmarks'
|
||||
// Allow `document.title` to be assigned in benchmarks without actually changing the window title.
|
||||
let documentTitle = null
|
||||
Object.defineProperty(document, 'title', {
|
||||
get () { return documentTitle },
|
||||
set (title) { documentTitle = title }
|
||||
})
|
||||
|
||||
window.addEventListener('keydown', (event) => {
|
||||
// Reload: cmd-r / ctrl-r
|
||||
if ((event.metaKey || event.ctrlKey) && event.keyCode === 82) {
|
||||
ipcHelpers.call('window-method', 'reload')
|
||||
}
|
||||
|
||||
// Toggle Dev Tools: cmd-alt-i (Mac) / ctrl-shift-i (Linux/Windows)
|
||||
if (event.keyCode === 73) {
|
||||
const isDarwin = process.platform === 'darwin'
|
||||
if ((isDarwin && event.metaKey && event.altKey) || (!isDarwin && event.ctrlKey && event.shiftKey)) {
|
||||
ipcHelpers.call('window-method', 'toggleDevTools')
|
||||
}
|
||||
}
|
||||
|
||||
// Close: cmd-w / ctrl-w
|
||||
if ((event.metaKey || event.ctrlKey) && event.keyCode === 87) {
|
||||
ipcHelpers.call('window-method', 'close')
|
||||
}
|
||||
|
||||
// Copy: cmd-c / ctrl-c
|
||||
if ((event.metaKey || event.ctrlKey) && event.keyCode === 67) {
|
||||
ipcHelpers.call('window-method', 'copy')
|
||||
}
|
||||
}, true)
|
||||
|
||||
const clipboard = new Clipboard()
|
||||
TextEditor.setClipboard(clipboard)
|
||||
TextEditor.viewForItem = (item) => atom.views.getView(item)
|
||||
|
||||
const applicationDelegate = new ApplicationDelegate()
|
||||
const environmentParams = {
|
||||
applicationDelegate,
|
||||
window,
|
||||
document,
|
||||
clipboard,
|
||||
configDirPath: process.env.ATOM_HOME,
|
||||
enablePersistence: false
|
||||
}
|
||||
global.atom = new AtomEnvironment(environmentParams)
|
||||
global.atom.initialize(environmentParams)
|
||||
|
||||
// Prevent benchmarks from modifying application menus
|
||||
global.atom.menu.sendToBrowserProcess = function () { }
|
||||
|
||||
if (headless) {
|
||||
Object.defineProperties(process, {
|
||||
stdout: { value: remote.process.stdout },
|
||||
stderr: { value: remote.process.stderr }
|
||||
})
|
||||
|
||||
console.log = function (...args) {
|
||||
const formatted = util.format(...args)
|
||||
process.stdout.write(formatted + '\n')
|
||||
}
|
||||
console.warn = function (...args) {
|
||||
const formatted = util.format(...args)
|
||||
process.stderr.write(formatted + '\n')
|
||||
}
|
||||
console.error = function (...args) {
|
||||
const formatted = util.format(...args)
|
||||
process.stderr.write(formatted + '\n')
|
||||
}
|
||||
} else {
|
||||
remote.getCurrentWindow().show()
|
||||
}
|
||||
|
||||
const benchmarkRunner = require('../benchmarks/benchmark-runner')
|
||||
const statusCode = await benchmarkRunner({test, benchmarkPaths})
|
||||
if (headless) {
|
||||
exitWithStatusCode(statusCode)
|
||||
}
|
||||
} catch (error) {
|
||||
if (headless) {
|
||||
console.error(error.stack || error)
|
||||
exitWithStatusCode(1)
|
||||
} else {
|
||||
ipcHelpers.call('window-method', 'openDevTools')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function exitWithStatusCode (statusCode) {
|
||||
remote.app.emit('will-quit')
|
||||
remote.process.exit(statusCode)
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
ipcHelpers = require './ipc-helpers'
|
||||
|
||||
cloneObject = (object) ->
|
||||
clone = {}
|
||||
clone[key] = value for key, value of object
|
||||
clone
|
||||
|
||||
module.exports = ({blobStore}) ->
|
||||
{crashReporter, remote} = require 'electron'
|
||||
# Start the crash reporter before anything else.
|
||||
crashReporter.start(productName: 'Atom', companyName: 'GitHub', submitURL: 'http://54.249.141.255:1127/post')
|
||||
startCrashReporter = require('./crash-reporter-start')
|
||||
{remote} = require 'electron'
|
||||
|
||||
startCrashReporter() # Before anything else
|
||||
|
||||
exitWithStatusCode = (status) ->
|
||||
remote.app.emit('will-quit')
|
||||
@@ -15,11 +18,19 @@ module.exports = ({blobStore}) ->
|
||||
try
|
||||
path = require 'path'
|
||||
{ipcRenderer} = require 'electron'
|
||||
{getWindowLoadSettings} = require './window-load-settings-helpers'
|
||||
getWindowLoadSettings = require './get-window-load-settings'
|
||||
CompileCache = require './compile-cache'
|
||||
AtomEnvironment = require '../src/atom-environment'
|
||||
ApplicationDelegate = require '../src/application-delegate'
|
||||
Clipboard = require '../src/clipboard'
|
||||
TextEditor = require '../src/text-editor'
|
||||
{updateProcessEnv} = require('./update-process-env')
|
||||
require './electron-shims'
|
||||
|
||||
{testRunnerPath, legacyTestRunnerPath, headless, logFile, testPaths} = getWindowLoadSettings()
|
||||
ipcRenderer.on 'environment', (event, env) ->
|
||||
updateProcessEnv(env)
|
||||
|
||||
{testRunnerPath, legacyTestRunnerPath, headless, logFile, testPaths, env} = getWindowLoadSettings()
|
||||
|
||||
unless headless
|
||||
# Show window synchronously so a focusout doesn't fire on input elements
|
||||
@@ -29,15 +40,21 @@ module.exports = ({blobStore}) ->
|
||||
handleKeydown = (event) ->
|
||||
# Reload: cmd-r / ctrl-r
|
||||
if (event.metaKey or event.ctrlKey) and event.keyCode is 82
|
||||
ipcRenderer.send('call-window-method', 'reload')
|
||||
ipcHelpers.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
|
||||
ipcRenderer.send('call-window-method', 'toggleDevTools')
|
||||
# Toggle Dev Tools: cmd-alt-i (Mac) / ctrl-shift-i (Linux/Windows)
|
||||
if event.keyCode is 73 and (
|
||||
(process.platform is 'darwin' and event.metaKey and event.altKey) or
|
||||
(process.platform isnt 'darwin' and event.ctrlKey and event.shiftKey))
|
||||
ipcHelpers.call('window-method', 'toggleDevTools')
|
||||
|
||||
# Reload: cmd-w / ctrl-w
|
||||
# Close: cmd-w / ctrl-w
|
||||
if (event.metaKey or event.ctrlKey) and event.keyCode is 87
|
||||
ipcRenderer.send('call-window-method', 'close')
|
||||
ipcHelpers.call('window-method', 'close')
|
||||
|
||||
# Copy: cmd-c / ctrl-c
|
||||
if (event.metaKey or event.ctrlKey) and event.keyCode is 67
|
||||
ipcHelpers.call('window-method', 'copy')
|
||||
|
||||
window.addEventListener('keydown', handleKeydown, true)
|
||||
|
||||
@@ -46,23 +63,33 @@ module.exports = ({blobStore}) ->
|
||||
require('module').globalPaths.push(exportsPath)
|
||||
process.env.NODE_PATH = exportsPath # Set NODE_PATH env variable since tasks may need it.
|
||||
|
||||
updateProcessEnv(env)
|
||||
|
||||
# Set up optional transpilation for packages under test if any
|
||||
FindParentDir = require 'find-parent-dir'
|
||||
if packageRoot = FindParentDir.sync(testPaths[0], 'package.json')
|
||||
packageMetadata = require(path.join(packageRoot, 'package.json'))
|
||||
if packageMetadata.atomTranspilers
|
||||
CompileCache.addTranspilerConfigForPath(packageRoot, packageMetadata.name, packageMetadata, packageMetadata.atomTranspilers)
|
||||
|
||||
document.title = "Spec Suite"
|
||||
|
||||
# Avoid throttling of test window by playing silence
|
||||
# See related discussion in https://github.com/atom/atom/pull/9485
|
||||
context = new AudioContext()
|
||||
source = context.createBufferSource()
|
||||
source.connect(context.destination)
|
||||
source.start(0)
|
||||
clipboard = new Clipboard
|
||||
TextEditor.setClipboard(clipboard)
|
||||
TextEditor.viewForItem = (item) -> atom.views.getView(item)
|
||||
|
||||
testRunner = require(testRunnerPath)
|
||||
legacyTestRunner = require(legacyTestRunnerPath)
|
||||
buildDefaultApplicationDelegate = -> new ApplicationDelegate()
|
||||
buildAtomEnvironment = (params) ->
|
||||
params = cloneObject(params)
|
||||
params.clipboard = clipboard unless params.hasOwnProperty("clipboard")
|
||||
params.blobStore = blobStore unless params.hasOwnProperty("blobStore")
|
||||
params.onlyLoadBaseStyleSheets = true unless params.hasOwnProperty("onlyLoadBaseStyleSheets")
|
||||
new AtomEnvironment(params)
|
||||
atomEnvironment = new AtomEnvironment(params)
|
||||
atomEnvironment.initialize(params)
|
||||
TextEditor.setScheduler(atomEnvironment.views)
|
||||
atomEnvironment
|
||||
|
||||
promise = testRunner({
|
||||
logFile, headless, testPaths, buildAtomEnvironment, buildDefaultApplicationDelegate, legacyTestRunner
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
module.exports =
|
||||
class InputComponent
|
||||
constructor: ->
|
||||
@domNode = document.createElement('input')
|
||||
@domNode.classList.add('hidden-input')
|
||||
@domNode.setAttribute('tabindex', -1)
|
||||
@domNode.setAttribute('data-react-skip-selection-restoration', true)
|
||||
@domNode.style['-webkit-transform'] = 'translateZ(0)'
|
||||
@domNode.addEventListener 'paste', (event) -> event.preventDefault()
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
updateSync: (state) ->
|
||||
@oldState ?= {}
|
||||
newState = state.hiddenInput
|
||||
|
||||
if newState.top isnt @oldState.top
|
||||
@domNode.style.top = newState.top + 'px'
|
||||
@oldState.top = newState.top
|
||||
|
||||
if newState.left isnt @oldState.left
|
||||
@domNode.style.left = newState.left + 'px'
|
||||
@oldState.left = newState.left
|
||||
|
||||
if newState.width isnt @oldState.width
|
||||
@domNode.style.width = newState.width + 'px'
|
||||
@oldState.width = newState.width
|
||||
|
||||
if newState.height isnt @oldState.height
|
||||
@domNode.style.height = newState.height + 'px'
|
||||
@oldState.height = newState.height
|
||||
@@ -1,40 +1,43 @@
|
||||
var ipcRenderer = null
|
||||
var ipcMain = null
|
||||
var BrowserWindow = null
|
||||
const Disposable = require('event-kit').Disposable
|
||||
let ipcRenderer = null
|
||||
let ipcMain = null
|
||||
let BrowserWindow = null
|
||||
|
||||
exports.call = function (methodName, ...args) {
|
||||
let nextResponseChannelId = 0
|
||||
|
||||
exports.on = function (emitter, eventName, callback) {
|
||||
emitter.on(eventName, callback)
|
||||
return new Disposable(() => emitter.removeListener(eventName, callback))
|
||||
}
|
||||
|
||||
exports.call = function (channel, ...args) {
|
||||
if (!ipcRenderer) {
|
||||
ipcRenderer = require('electron').ipcRenderer
|
||||
ipcRenderer.setMaxListeners(20)
|
||||
}
|
||||
|
||||
var responseChannel = getResponseChannel(methodName)
|
||||
const responseChannel = `ipc-helpers-response-${nextResponseChannelId++}`
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
ipcRenderer.on(responseChannel, function (event, result) {
|
||||
return new Promise(resolve => {
|
||||
ipcRenderer.on(responseChannel, (event, result) => {
|
||||
ipcRenderer.removeAllListeners(responseChannel)
|
||||
resolve(result)
|
||||
})
|
||||
|
||||
ipcRenderer.send(methodName, ...args)
|
||||
ipcRenderer.send(channel, responseChannel, ...args)
|
||||
})
|
||||
}
|
||||
|
||||
exports.respondTo = function (methodName, callback) {
|
||||
exports.respondTo = function (channel, callback) {
|
||||
if (!ipcMain) {
|
||||
var electron = require('electron')
|
||||
const 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)
|
||||
return exports.on(ipcMain, channel, async (event, responseChannel, ...args) => {
|
||||
const browserWindow = BrowserWindow.fromWebContents(event.sender)
|
||||
const result = await callback(browserWindow, ...args)
|
||||
event.sender.send(responseChannel, result)
|
||||
})
|
||||
}
|
||||
|
||||
function getResponseChannel (methodName) {
|
||||
return 'ipc-helpers-' + methodName + '-response'
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
module.exports =
|
||||
class ItemRegistry
|
||||
constructor: ->
|
||||
@items = new WeakSet
|
||||
|
||||
addItem: (item) ->
|
||||
if @hasItem(item)
|
||||
throw new Error("The workspace can only contain one instance of item #{item}")
|
||||
@items.add(item)
|
||||
|
||||
removeItem: (item) ->
|
||||
@items.delete(item)
|
||||
|
||||
hasItem: (item) ->
|
||||
@items.has(item)
|
||||
21
src/item-registry.js
Normal file
21
src/item-registry.js
Normal file
@@ -0,0 +1,21 @@
|
||||
module.exports =
|
||||
class ItemRegistry {
|
||||
constructor () {
|
||||
this.items = new WeakSet()
|
||||
}
|
||||
|
||||
addItem (item) {
|
||||
if (this.hasItem(item)) {
|
||||
throw new Error(`The workspace can only contain one instance of item ${item}`)
|
||||
}
|
||||
return this.items.add(item)
|
||||
}
|
||||
|
||||
removeItem (item) {
|
||||
return this.items.delete(item)
|
||||
}
|
||||
|
||||
hasItem (item) {
|
||||
return this.items.has(item)
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,19 @@ bundledKeymaps = require('../package.json')?._atomKeymaps
|
||||
KeymapManager::onDidLoadBundledKeymaps = (callback) ->
|
||||
@emitter.on 'did-load-bundled-keymaps', callback
|
||||
|
||||
KeymapManager::onDidLoadUserKeymap = (callback) ->
|
||||
@emitter.on 'did-load-user-keymap', callback
|
||||
|
||||
KeymapManager::canLoadBundledKeymapsFromMemory = ->
|
||||
bundledKeymaps?
|
||||
|
||||
KeymapManager::loadBundledKeymaps = ->
|
||||
keymapsPath = path.join(@resourcePath, 'keymaps')
|
||||
if bundledKeymaps?
|
||||
for keymapName, keymap of bundledKeymaps
|
||||
keymapPath = path.join(keymapsPath, keymapName)
|
||||
@add(keymapPath, keymap)
|
||||
keymapPath = "core:#{keymapName}"
|
||||
@add(keymapPath, keymap, 0, @devMode ? false)
|
||||
else
|
||||
keymapsPath = path.join(@resourcePath, 'keymaps')
|
||||
@loadKeymap(keymapsPath)
|
||||
|
||||
@emitter.emit 'did-load-bundled-keymaps'
|
||||
@@ -49,6 +55,9 @@ KeymapManager::loadUserKeymap = ->
|
||||
stack = error.stack
|
||||
@notificationManager.addFatalError(error.message, {detail, stack, dismissable: true})
|
||||
|
||||
@emitter.emit 'did-load-user-keymap'
|
||||
|
||||
|
||||
KeymapManager::subscribeToFileReadFailure = ->
|
||||
@onDidFailToReadFile (error) =>
|
||||
userKeymapPath = @getUserKeymapPath()
|
||||
|
||||
@@ -1,351 +0,0 @@
|
||||
{Range} = require 'text-buffer'
|
||||
_ = require 'underscore-plus'
|
||||
{OnigRegExp} = require 'oniguruma'
|
||||
ScopeDescriptor = require './scope-descriptor'
|
||||
|
||||
module.exports =
|
||||
class LanguageMode
|
||||
# Sets up a `LanguageMode` for the given {TextEditor}.
|
||||
#
|
||||
# editor - The {TextEditor} to associate with
|
||||
constructor: (@editor, @config) ->
|
||||
{@buffer} = @editor
|
||||
@regexesByPattern = {}
|
||||
|
||||
destroy: ->
|
||||
|
||||
toggleLineCommentForBufferRow: (row) ->
|
||||
@toggleLineCommentsForBufferRows(row, row)
|
||||
|
||||
# Wraps the lines between two rows in comments.
|
||||
#
|
||||
# If the language doesn't have comment, nothing happens.
|
||||
#
|
||||
# startRow - The row {Number} to start at
|
||||
# endRow - The row {Number} to end at
|
||||
toggleLineCommentsForBufferRows: (start, end) ->
|
||||
scope = @editor.scopeDescriptorForBufferPosition([start, 0])
|
||||
{commentStartString, commentEndString} = @commentStartAndEndStringsForScope(scope)
|
||||
return unless commentStartString?
|
||||
|
||||
buffer = @editor.buffer
|
||||
commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?')
|
||||
commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})")
|
||||
|
||||
if commentEndString
|
||||
shouldUncomment = commentStartRegex.testSync(buffer.lineForRow(start))
|
||||
if shouldUncomment
|
||||
commentEndRegexString = _.escapeRegExp(commentEndString).replace(/^(\s+)/, '(?:$1)?')
|
||||
commentEndRegex = new OnigRegExp("(#{commentEndRegexString})(\\s*)$")
|
||||
startMatch = commentStartRegex.searchSync(buffer.lineForRow(start))
|
||||
endMatch = commentEndRegex.searchSync(buffer.lineForRow(end))
|
||||
if startMatch and endMatch
|
||||
buffer.transact ->
|
||||
columnStart = startMatch[1].length
|
||||
columnEnd = columnStart + startMatch[2].length
|
||||
buffer.setTextInRange([[start, columnStart], [start, columnEnd]], "")
|
||||
|
||||
endLength = buffer.lineLengthForRow(end) - endMatch[2].length
|
||||
endColumn = endLength - endMatch[1].length
|
||||
buffer.setTextInRange([[end, endColumn], [end, endLength]], "")
|
||||
else
|
||||
buffer.transact ->
|
||||
indentLength = buffer.lineForRow(start).match(/^\s*/)?[0].length ? 0
|
||||
buffer.insert([start, indentLength], commentStartString)
|
||||
buffer.insert([end, buffer.lineLengthForRow(end)], commentEndString)
|
||||
else
|
||||
allBlank = true
|
||||
allBlankOrCommented = true
|
||||
|
||||
for row in [start..end] by 1
|
||||
line = buffer.lineForRow(row)
|
||||
blank = line?.match(/^\s*$/)
|
||||
|
||||
allBlank = false unless blank
|
||||
allBlankOrCommented = false unless blank or commentStartRegex.testSync(line)
|
||||
|
||||
shouldUncomment = allBlankOrCommented and not allBlank
|
||||
|
||||
if shouldUncomment
|
||||
for row in [start..end] by 1
|
||||
if match = commentStartRegex.searchSync(buffer.lineForRow(row))
|
||||
columnStart = match[1].length
|
||||
columnEnd = columnStart + match[2].length
|
||||
buffer.setTextInRange([[row, columnStart], [row, columnEnd]], "")
|
||||
else
|
||||
if start is end
|
||||
indent = @editor.indentationForBufferRow(start)
|
||||
else
|
||||
indent = @minIndentLevelForRowRange(start, end)
|
||||
indentString = @editor.buildIndentString(indent)
|
||||
tabLength = @editor.getTabLength()
|
||||
indentRegex = new RegExp("(\t|[ ]{#{tabLength}}){#{Math.floor(indent)}}")
|
||||
for row in [start..end] by 1
|
||||
line = buffer.lineForRow(row)
|
||||
if indentLength = line.match(indentRegex)?[0].length
|
||||
buffer.insert([row, indentLength], commentStartString)
|
||||
else
|
||||
buffer.setTextInRange([[row, 0], [row, indentString.length]], indentString + commentStartString)
|
||||
return
|
||||
|
||||
# Folds all the foldable lines in the buffer.
|
||||
foldAll: ->
|
||||
for currentRow in [0..@buffer.getLastRow()] by 1
|
||||
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
|
||||
continue unless startRow?
|
||||
@editor.createFold(startRow, endRow)
|
||||
return
|
||||
|
||||
# Unfolds all the foldable lines in the buffer.
|
||||
unfoldAll: ->
|
||||
for fold in @editor.displayBuffer.foldsIntersectingBufferRowRange(0, @buffer.getLastRow()) by -1
|
||||
fold.destroy()
|
||||
return
|
||||
|
||||
# Fold all comment and code blocks at a given indentLevel
|
||||
#
|
||||
# indentLevel - A {Number} indicating indentLevel; 0 based.
|
||||
foldAllAtIndentLevel: (indentLevel) ->
|
||||
@unfoldAll()
|
||||
for currentRow in [0..@buffer.getLastRow()] by 1
|
||||
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
|
||||
continue unless startRow?
|
||||
|
||||
# assumption: startRow will always be the min indent level for the entire range
|
||||
if @editor.indentationForBufferRow(startRow) is indentLevel
|
||||
@editor.createFold(startRow, endRow)
|
||||
return
|
||||
|
||||
# Given a buffer row, creates a fold at it.
|
||||
#
|
||||
# bufferRow - A {Number} indicating the buffer row
|
||||
#
|
||||
# Returns the new {Fold}.
|
||||
foldBufferRow: (bufferRow) ->
|
||||
for currentRow in [bufferRow..0] by -1
|
||||
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
|
||||
continue unless startRow? and startRow <= bufferRow <= endRow
|
||||
fold = @editor.displayBuffer.largestFoldStartingAtBufferRow(startRow)
|
||||
return @editor.createFold(startRow, endRow) unless fold
|
||||
|
||||
# Find the row range for a fold at a given bufferRow. Will handle comments
|
||||
# and code.
|
||||
#
|
||||
# bufferRow - A {Number} indicating the buffer row
|
||||
#
|
||||
# Returns an {Array} of the [startRow, endRow]. Returns null if no range.
|
||||
rowRangeForFoldAtBufferRow: (bufferRow) ->
|
||||
rowRange = @rowRangeForCommentAtBufferRow(bufferRow)
|
||||
rowRange ?= @rowRangeForCodeFoldAtBufferRow(bufferRow)
|
||||
rowRange
|
||||
|
||||
rowRangeForCommentAtBufferRow: (bufferRow) ->
|
||||
return unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
|
||||
|
||||
startRow = bufferRow
|
||||
endRow = bufferRow
|
||||
|
||||
if bufferRow > 0
|
||||
for currentRow in [bufferRow-1..0] by -1
|
||||
break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
|
||||
startRow = currentRow
|
||||
|
||||
if bufferRow < @buffer.getLastRow()
|
||||
for currentRow in [bufferRow+1..@buffer.getLastRow()] by 1
|
||||
break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
|
||||
endRow = currentRow
|
||||
|
||||
return [startRow, endRow] if startRow isnt endRow
|
||||
|
||||
rowRangeForCodeFoldAtBufferRow: (bufferRow) ->
|
||||
return null unless @isFoldableAtBufferRow(bufferRow)
|
||||
|
||||
startIndentLevel = @editor.indentationForBufferRow(bufferRow)
|
||||
scopeDescriptor = @editor.scopeDescriptorForBufferPosition([bufferRow, 0])
|
||||
for row in [(bufferRow + 1)..@editor.getLastBufferRow()] by 1
|
||||
continue if @editor.isBufferRowBlank(row)
|
||||
indentation = @editor.indentationForBufferRow(row)
|
||||
if indentation <= startIndentLevel
|
||||
includeRowInFold = indentation is startIndentLevel and @foldEndRegexForScopeDescriptor(scopeDescriptor)?.searchSync(@editor.lineTextForBufferRow(row))
|
||||
foldEndRow = row if includeRowInFold
|
||||
break
|
||||
|
||||
foldEndRow = row
|
||||
|
||||
[bufferRow, foldEndRow]
|
||||
|
||||
isFoldableAtBufferRow: (bufferRow) ->
|
||||
@editor.displayBuffer.tokenizedBuffer.isFoldableAtRow(bufferRow)
|
||||
|
||||
# Returns a {Boolean} indicating whether the line at the given buffer
|
||||
# row is a comment.
|
||||
isLineCommentedAtBufferRow: (bufferRow) ->
|
||||
return false unless 0 <= bufferRow <= @editor.getLastBufferRow()
|
||||
@editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
|
||||
|
||||
# Find a row range for a 'paragraph' around specified bufferRow. A paragraph
|
||||
# is a block of text bounded by and empty line or a block of text that is not
|
||||
# the same type (comments next to source code).
|
||||
rowRangeForParagraphAtBufferRow: (bufferRow) ->
|
||||
scope = @editor.scopeDescriptorForBufferPosition([bufferRow, 0])
|
||||
{commentStartString, commentEndString} = @commentStartAndEndStringsForScope(scope)
|
||||
commentStartRegex = null
|
||||
if commentStartString? and not commentEndString?
|
||||
commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?')
|
||||
commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})")
|
||||
|
||||
filterCommentStart = (line) ->
|
||||
if commentStartRegex?
|
||||
matches = commentStartRegex.searchSync(line)
|
||||
line = line.substring(matches[0].end) if matches?.length
|
||||
line
|
||||
|
||||
return unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(bufferRow)))
|
||||
|
||||
if @isLineCommentedAtBufferRow(bufferRow)
|
||||
isOriginalRowComment = true
|
||||
range = @rowRangeForCommentAtBufferRow(bufferRow)
|
||||
[firstRow, lastRow] = range or [bufferRow, bufferRow]
|
||||
else
|
||||
isOriginalRowComment = false
|
||||
[firstRow, lastRow] = [0, @editor.getLastBufferRow()-1]
|
||||
|
||||
startRow = bufferRow
|
||||
while startRow > firstRow
|
||||
break if @isLineCommentedAtBufferRow(startRow - 1) isnt isOriginalRowComment
|
||||
break unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(startRow - 1)))
|
||||
startRow--
|
||||
|
||||
endRow = bufferRow
|
||||
lastRow = @editor.getLastBufferRow()
|
||||
while endRow < lastRow
|
||||
break if @isLineCommentedAtBufferRow(endRow + 1) isnt isOriginalRowComment
|
||||
break unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(endRow + 1)))
|
||||
endRow++
|
||||
|
||||
new Range([startRow, 0], [endRow, @editor.lineTextForBufferRow(endRow).length])
|
||||
|
||||
# Given a buffer row, this returns a suggested indentation level.
|
||||
#
|
||||
# The indentation level provided is based on the current {LanguageMode}.
|
||||
#
|
||||
# bufferRow - A {Number} indicating the buffer row
|
||||
#
|
||||
# Returns a {Number}.
|
||||
suggestedIndentForBufferRow: (bufferRow, options) ->
|
||||
line = @buffer.lineForRow(bufferRow)
|
||||
tokenizedLine = @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow)
|
||||
@suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
|
||||
|
||||
suggestedIndentForLineAtBufferRow: (bufferRow, line, options) ->
|
||||
tokenizedLine = @editor.displayBuffer.tokenizedBuffer.buildTokenizedLineForRowWithText(bufferRow, line)
|
||||
@suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
|
||||
|
||||
suggestedIndentForTokenizedLineAtBufferRow: (bufferRow, line, tokenizedLine, options) ->
|
||||
iterator = tokenizedLine.getTokenIterator()
|
||||
iterator.next()
|
||||
scopeDescriptor = new ScopeDescriptor(scopes: iterator.getScopes())
|
||||
|
||||
increaseIndentRegex = @increaseIndentRegexForScopeDescriptor(scopeDescriptor)
|
||||
decreaseIndentRegex = @decreaseIndentRegexForScopeDescriptor(scopeDescriptor)
|
||||
decreaseNextIndentRegex = @decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor)
|
||||
|
||||
if options?.skipBlankLines ? true
|
||||
precedingRow = @buffer.previousNonBlankRow(bufferRow)
|
||||
return 0 unless precedingRow?
|
||||
else
|
||||
precedingRow = bufferRow - 1
|
||||
return 0 if precedingRow < 0
|
||||
|
||||
desiredIndentLevel = @editor.indentationForBufferRow(precedingRow)
|
||||
return desiredIndentLevel unless increaseIndentRegex
|
||||
|
||||
unless @editor.isBufferRowCommented(precedingRow)
|
||||
precedingLine = @buffer.lineForRow(precedingRow)
|
||||
desiredIndentLevel += 1 if increaseIndentRegex?.testSync(precedingLine)
|
||||
desiredIndentLevel -= 1 if decreaseNextIndentRegex?.testSync(precedingLine)
|
||||
|
||||
unless @buffer.isRowBlank(precedingRow)
|
||||
desiredIndentLevel -= 1 if decreaseIndentRegex?.testSync(line)
|
||||
|
||||
Math.max(desiredIndentLevel, 0)
|
||||
|
||||
# Calculate a minimum indent level for a range of lines excluding empty lines.
|
||||
#
|
||||
# startRow - The row {Number} to start at
|
||||
# endRow - The row {Number} to end at
|
||||
#
|
||||
# Returns a {Number} of the indent level of the block of lines.
|
||||
minIndentLevelForRowRange: (startRow, endRow) ->
|
||||
indents = (@editor.indentationForBufferRow(row) for row in [startRow..endRow] by 1 when not @editor.isBufferRowBlank(row))
|
||||
indents = [0] unless indents.length
|
||||
Math.min(indents...)
|
||||
|
||||
# Indents all the rows between two buffer row numbers.
|
||||
#
|
||||
# startRow - The row {Number} to start at
|
||||
# endRow - The row {Number} to end at
|
||||
autoIndentBufferRows: (startRow, endRow) ->
|
||||
@autoIndentBufferRow(row) for row in [startRow..endRow] by 1
|
||||
return
|
||||
|
||||
# Given a buffer row, this indents it.
|
||||
#
|
||||
# bufferRow - The row {Number}.
|
||||
# options - An options {Object} to pass through to {TextEditor::setIndentationForBufferRow}.
|
||||
autoIndentBufferRow: (bufferRow, options) ->
|
||||
indentLevel = @suggestedIndentForBufferRow(bufferRow, options)
|
||||
@editor.setIndentationForBufferRow(bufferRow, indentLevel, options)
|
||||
|
||||
# Given a buffer row, this decreases the indentation.
|
||||
#
|
||||
# bufferRow - The row {Number}
|
||||
autoDecreaseIndentForBufferRow: (bufferRow) ->
|
||||
scopeDescriptor = @editor.scopeDescriptorForBufferPosition([bufferRow, 0])
|
||||
return unless decreaseIndentRegex = @decreaseIndentRegexForScopeDescriptor(scopeDescriptor)
|
||||
|
||||
line = @buffer.lineForRow(bufferRow)
|
||||
return unless decreaseIndentRegex.testSync(line)
|
||||
|
||||
currentIndentLevel = @editor.indentationForBufferRow(bufferRow)
|
||||
return if currentIndentLevel is 0
|
||||
|
||||
precedingRow = @buffer.previousNonBlankRow(bufferRow)
|
||||
return unless precedingRow?
|
||||
|
||||
precedingLine = @buffer.lineForRow(precedingRow)
|
||||
desiredIndentLevel = @editor.indentationForBufferRow(precedingRow)
|
||||
|
||||
if increaseIndentRegex = @increaseIndentRegexForScopeDescriptor(scopeDescriptor)
|
||||
desiredIndentLevel -= 1 unless increaseIndentRegex.testSync(precedingLine)
|
||||
|
||||
if decreaseNextIndentRegex = @decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor)
|
||||
desiredIndentLevel -= 1 if decreaseNextIndentRegex.testSync(precedingLine)
|
||||
|
||||
if desiredIndentLevel >= 0 and desiredIndentLevel < currentIndentLevel
|
||||
@editor.setIndentationForBufferRow(bufferRow, desiredIndentLevel)
|
||||
|
||||
getRegexForProperty: (scopeDescriptor, property) ->
|
||||
if pattern = @config.get(property, scope: scopeDescriptor)
|
||||
@regexesByPattern[pattern] ?= new OnigRegExp(pattern)
|
||||
@regexesByPattern[pattern]
|
||||
|
||||
increaseIndentRegexForScopeDescriptor: (scopeDescriptor) ->
|
||||
@getRegexForProperty(scopeDescriptor, 'editor.increaseIndentPattern')
|
||||
|
||||
decreaseIndentRegexForScopeDescriptor: (scopeDescriptor) ->
|
||||
@getRegexForProperty(scopeDescriptor, 'editor.decreaseIndentPattern')
|
||||
|
||||
decreaseNextIndentRegexForScopeDescriptor: (scopeDescriptor) ->
|
||||
@getRegexForProperty(scopeDescriptor, 'editor.decreaseNextIndentPattern')
|
||||
|
||||
foldEndRegexForScopeDescriptor: (scopeDescriptor) ->
|
||||
@getRegexForProperty(scopeDescriptor, 'editor.foldEndPattern')
|
||||
|
||||
commentStartAndEndStringsForScope: (scope) ->
|
||||
commentStartEntry = @config.getAll('editor.commentStart', {scope})[0]
|
||||
commentEndEntry = _.find @config.getAll('editor.commentEnd', {scope}), (entry) ->
|
||||
entry.scopeSelector is commentStartEntry.scopeSelector
|
||||
commentStartString = commentStartEntry?.value
|
||||
commentEndString = commentEndEntry?.value
|
||||
{commentStartString, commentEndString}
|
||||
@@ -1,5 +1,3 @@
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
idCounter = 0
|
||||
nextId = -> idCounter++
|
||||
|
||||
@@ -7,11 +5,11 @@ nextId = -> idCounter++
|
||||
# layer. Created via {TextEditor::decorateMarkerLayer}.
|
||||
module.exports =
|
||||
class LayerDecoration
|
||||
constructor: (@markerLayer, @displayBuffer, @properties) ->
|
||||
constructor: (@markerLayer, @decorationManager, @properties) ->
|
||||
@id = nextId()
|
||||
@destroyed = false
|
||||
@markerLayerDestroyedDisposable = @markerLayer.onDidDestroy => @destroy()
|
||||
@overridePropertiesByMarkerId = {}
|
||||
@overridePropertiesByMarker = null
|
||||
|
||||
# Essential: Destroys the decoration.
|
||||
destroy: ->
|
||||
@@ -19,7 +17,7 @@ class LayerDecoration
|
||||
@markerLayerDestroyedDisposable.dispose()
|
||||
@markerLayerDestroyedDisposable = null
|
||||
@destroyed = true
|
||||
@displayBuffer.didDestroyLayerDecoration(this)
|
||||
@decorationManager.didDestroyLayerDecoration(this)
|
||||
|
||||
# Essential: Determine whether this decoration is destroyed.
|
||||
#
|
||||
@@ -44,18 +42,23 @@ class LayerDecoration
|
||||
setProperties: (newProperties) ->
|
||||
return if @destroyed
|
||||
@properties = newProperties
|
||||
@displayBuffer.scheduleUpdateDecorationsEvent()
|
||||
@decorationManager.emitDidUpdateDecorations()
|
||||
|
||||
# Essential: Override the decoration properties for a specific marker.
|
||||
#
|
||||
# * `marker` The {TextEditorMarker} or {Marker} for which to override
|
||||
# * `marker` The {DisplayMarker} or {Marker} for which to override
|
||||
# properties.
|
||||
# * `properties` An {Object} containing properties to apply to this marker.
|
||||
# Pass `null` to clear the override.
|
||||
setPropertiesForMarker: (marker, properties) ->
|
||||
return if @destroyed
|
||||
@overridePropertiesByMarker ?= new Map()
|
||||
marker = @markerLayer.getMarker(marker.id)
|
||||
if properties?
|
||||
@overridePropertiesByMarkerId[marker.id] = properties
|
||||
@overridePropertiesByMarker.set(marker, properties)
|
||||
else
|
||||
delete @overridePropertiesByMarkerId[marker.id]
|
||||
@displayBuffer.scheduleUpdateDecorationsEvent()
|
||||
@overridePropertiesByMarker.delete(marker)
|
||||
@decorationManager.emitDidUpdateDecorations()
|
||||
|
||||
getPropertiesForMarker: (marker) ->
|
||||
@overridePropertiesByMarker?.get(marker)
|
||||
|
||||
@@ -4,9 +4,9 @@ LessCache = require 'less-cache'
|
||||
# {LessCache} wrapper used by {ThemeManager} to read stylesheets.
|
||||
module.exports =
|
||||
class LessCompileCache
|
||||
@cacheDir: path.join(process.env.ATOM_HOME, 'compile-cache', 'less')
|
||||
constructor: ({resourcePath, importPaths, lessSourcesByRelativeFilePath, importedFilePathsByRelativeImportPath}) ->
|
||||
cacheDir = path.join(process.env.ATOM_HOME, 'compile-cache', 'less')
|
||||
|
||||
constructor: ({resourcePath, importPaths}) ->
|
||||
@lessSearchPaths = [
|
||||
path.join(resourcePath, 'static', 'variables')
|
||||
path.join(resourcePath, 'static')
|
||||
@@ -17,11 +17,14 @@ class LessCompileCache
|
||||
else
|
||||
importPaths = @lessSearchPaths
|
||||
|
||||
@cache = new LessCache
|
||||
cacheDir: @constructor.cacheDir
|
||||
importPaths: importPaths
|
||||
resourcePath: resourcePath
|
||||
@cache = new LessCache({
|
||||
importPaths,
|
||||
resourcePath,
|
||||
lessSourcesByRelativeFilePath,
|
||||
importedFilePathsByRelativeImportPath,
|
||||
cacheDir,
|
||||
fallbackDir: path.join(resourcePath, 'less-compile-cache')
|
||||
})
|
||||
|
||||
setImportPaths: (importPaths=[]) ->
|
||||
@cache.setImportPaths(importPaths.concat(@lessSearchPaths))
|
||||
@@ -29,5 +32,5 @@ class LessCompileCache
|
||||
read: (stylesheetPath) ->
|
||||
@cache.readFileSync(stylesheetPath)
|
||||
|
||||
cssForFile: (stylesheetPath, lessContent) ->
|
||||
@cache.cssForFile(stylesheetPath, lessContent)
|
||||
cssForFile: (stylesheetPath, lessContent, digest) ->
|
||||
@cache.cssForFile(stylesheetPath, lessContent, digest)
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
TiledComponent = require './tiled-component'
|
||||
LineNumbersTileComponent = require './line-numbers-tile-component'
|
||||
WrapperDiv = document.createElement('div')
|
||||
DOMElementPool = require './dom-element-pool'
|
||||
|
||||
module.exports =
|
||||
class LineNumberGutterComponent extends TiledComponent
|
||||
dummyLineNumberNode: null
|
||||
|
||||
constructor: ({@onMouseDown, @editor, @gutter, @domElementPool, @views}) ->
|
||||
@visible = true
|
||||
|
||||
@dummyLineNumberComponent = LineNumbersTileComponent.createDummy(@domElementPool)
|
||||
|
||||
@domNode = @views.getView(@gutter)
|
||||
@lineNumbersNode = @domNode.firstChild
|
||||
@lineNumbersNode.innerHTML = ''
|
||||
|
||||
@domNode.addEventListener 'click', @onClick
|
||||
@domNode.addEventListener 'mousedown', @onMouseDown
|
||||
|
||||
destroy: ->
|
||||
@domNode.removeEventListener 'click', @onClick
|
||||
@domNode.removeEventListener 'mousedown', @onMouseDown
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
hideNode: ->
|
||||
if @visible
|
||||
@domNode.style.display = 'none'
|
||||
@visible = false
|
||||
|
||||
showNode: ->
|
||||
if not @visible
|
||||
@domNode.style.removeProperty('display')
|
||||
@visible = true
|
||||
|
||||
buildEmptyState: ->
|
||||
{
|
||||
tiles: {}
|
||||
styles: {}
|
||||
}
|
||||
|
||||
getNewState: (state) -> state
|
||||
|
||||
getTilesNode: -> @lineNumbersNode
|
||||
|
||||
beforeUpdateSync: (state) ->
|
||||
@appendDummyLineNumber() unless @dummyLineNumberNode?
|
||||
|
||||
if @newState.styles.maxHeight isnt @oldState.styles.maxHeight
|
||||
@lineNumbersNode.style.height = @newState.styles.maxHeight + 'px'
|
||||
@oldState.maxHeight = @newState.maxHeight
|
||||
|
||||
if @newState.styles.backgroundColor isnt @oldState.styles.backgroundColor
|
||||
@lineNumbersNode.style.backgroundColor = @newState.styles.backgroundColor
|
||||
@oldState.styles.backgroundColor = @newState.styles.backgroundColor
|
||||
|
||||
if @newState.maxLineNumberDigits isnt @oldState.maxLineNumberDigits
|
||||
@updateDummyLineNumber()
|
||||
@oldState.styles = {}
|
||||
@oldState.maxLineNumberDigits = @newState.maxLineNumberDigits
|
||||
|
||||
buildComponentForTile: (id) -> new LineNumbersTileComponent({id, @domElementPool})
|
||||
|
||||
shouldRecreateAllTilesOnUpdate: ->
|
||||
@newState.continuousReflow
|
||||
|
||||
###
|
||||
Section: Private Methods
|
||||
###
|
||||
|
||||
# This dummy line number element holds the gutter to the appropriate width,
|
||||
# since the real line numbers are absolutely positioned for performance reasons.
|
||||
appendDummyLineNumber: ->
|
||||
@dummyLineNumberComponent.newState = @newState
|
||||
@dummyLineNumberNode = @dummyLineNumberComponent.buildLineNumberNode({bufferRow: -1})
|
||||
@lineNumbersNode.appendChild(@dummyLineNumberNode)
|
||||
|
||||
updateDummyLineNumber: ->
|
||||
@dummyLineNumberComponent.newState = @newState
|
||||
@dummyLineNumberComponent.setLineNumberInnerNodes(0, false, @dummyLineNumberNode)
|
||||
|
||||
onMouseDown: (event) =>
|
||||
{target} = event
|
||||
lineNumber = target.parentNode
|
||||
|
||||
unless target.classList.contains('icon-right') and lineNumber.classList.contains('foldable')
|
||||
@onMouseDown(event)
|
||||
|
||||
onClick: (event) =>
|
||||
{target} = event
|
||||
lineNumber = target.parentNode
|
||||
|
||||
if target.classList.contains('icon-right') and lineNumber.classList.contains('foldable')
|
||||
bufferRow = parseInt(lineNumber.getAttribute('data-buffer-row'))
|
||||
if lineNumber.classList.contains('folded')
|
||||
@editor.unfoldBufferRow(bufferRow)
|
||||
else
|
||||
@editor.foldBufferRow(bufferRow)
|
||||
@@ -1,157 +0,0 @@
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
module.exports =
|
||||
class LineNumbersTileComponent
|
||||
@createDummy: (domElementPool) ->
|
||||
new LineNumbersTileComponent({id: -1, domElementPool})
|
||||
|
||||
constructor: ({@id, @domElementPool}) ->
|
||||
@lineNumberNodesById = {}
|
||||
@domNode = @domElementPool.buildElement("div")
|
||||
@domNode.style.position = "absolute"
|
||||
@domNode.style.display = "block"
|
||||
@domNode.style.top = 0 # Cover the space occupied by a dummy lineNumber
|
||||
|
||||
destroy: ->
|
||||
@domElementPool.freeElementAndDescendants(@domNode)
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
updateSync: (state) ->
|
||||
@newState = state
|
||||
unless @oldState
|
||||
@oldState = {tiles: {}, styles: {}}
|
||||
@oldState.tiles[@id] = {lineNumbers: {}}
|
||||
|
||||
@newTileState = @newState.tiles[@id]
|
||||
@oldTileState = @oldState.tiles[@id]
|
||||
|
||||
if @newTileState.display isnt @oldTileState.display
|
||||
@domNode.style.display = @newTileState.display
|
||||
@oldTileState.display = @newTileState.display
|
||||
|
||||
if @newState.styles.backgroundColor isnt @oldState.styles.backgroundColor
|
||||
@domNode.style.backgroundColor = @newState.styles.backgroundColor
|
||||
@oldState.styles.backgroundColor = @newState.styles.backgroundColor
|
||||
|
||||
if @newTileState.height isnt @oldTileState.height
|
||||
@domNode.style.height = @newTileState.height + 'px'
|
||||
@oldTileState.height = @newTileState.height
|
||||
|
||||
if @newTileState.top isnt @oldTileState.top
|
||||
@domNode.style['-webkit-transform'] = "translate3d(0, #{@newTileState.top}px, 0px)"
|
||||
@oldTileState.top = @newTileState.top
|
||||
|
||||
if @newTileState.zIndex isnt @oldTileState.zIndex
|
||||
@domNode.style.zIndex = @newTileState.zIndex
|
||||
@oldTileState.zIndex = @newTileState.zIndex
|
||||
|
||||
if @newState.maxLineNumberDigits isnt @oldState.maxLineNumberDigits
|
||||
for id, node of @lineNumberNodesById
|
||||
@domElementPool.freeElementAndDescendants(node)
|
||||
|
||||
@oldState.tiles[@id] = {lineNumbers: {}}
|
||||
@oldTileState = @oldState.tiles[@id]
|
||||
@lineNumberNodesById = {}
|
||||
@oldState.maxLineNumberDigits = @newState.maxLineNumberDigits
|
||||
|
||||
@updateLineNumbers()
|
||||
|
||||
updateLineNumbers: ->
|
||||
newLineNumberIds = null
|
||||
newLineNumberNodes = null
|
||||
|
||||
for id, lineNumberState of @oldTileState.lineNumbers
|
||||
unless @newTileState.lineNumbers.hasOwnProperty(id)
|
||||
@domElementPool.freeElementAndDescendants(@lineNumberNodesById[id])
|
||||
delete @lineNumberNodesById[id]
|
||||
delete @oldTileState.lineNumbers[id]
|
||||
|
||||
for id, lineNumberState of @newTileState.lineNumbers
|
||||
if @oldTileState.lineNumbers.hasOwnProperty(id)
|
||||
@updateLineNumberNode(id, lineNumberState)
|
||||
else
|
||||
newLineNumberIds ?= []
|
||||
newLineNumberNodes ?= []
|
||||
newLineNumberIds.push(id)
|
||||
newLineNumberNodes.push(@buildLineNumberNode(lineNumberState))
|
||||
@oldTileState.lineNumbers[id] = _.clone(lineNumberState)
|
||||
|
||||
return unless newLineNumberIds?
|
||||
|
||||
for id, i in newLineNumberIds
|
||||
lineNumberNode = newLineNumberNodes[i]
|
||||
@lineNumberNodesById[id] = lineNumberNode
|
||||
if nextNode = @findNodeNextTo(lineNumberNode)
|
||||
@domNode.insertBefore(lineNumberNode, nextNode)
|
||||
else
|
||||
@domNode.appendChild(lineNumberNode)
|
||||
|
||||
findNodeNextTo: (node) ->
|
||||
for nextNode in @domNode.children
|
||||
return nextNode if @screenRowForNode(node) < @screenRowForNode(nextNode)
|
||||
return
|
||||
|
||||
screenRowForNode: (node) -> parseInt(node.dataset.screenRow)
|
||||
|
||||
buildLineNumberNode: (lineNumberState) ->
|
||||
{screenRow, bufferRow, softWrapped, top, decorationClasses, zIndex, blockDecorationsHeight} = lineNumberState
|
||||
|
||||
className = @buildLineNumberClassName(lineNumberState)
|
||||
lineNumberNode = @domElementPool.buildElement("div", className)
|
||||
lineNumberNode.dataset.screenRow = screenRow
|
||||
lineNumberNode.dataset.bufferRow = bufferRow
|
||||
lineNumberNode.style.marginTop = blockDecorationsHeight + "px"
|
||||
|
||||
@setLineNumberInnerNodes(bufferRow, softWrapped, lineNumberNode)
|
||||
lineNumberNode
|
||||
|
||||
setLineNumberInnerNodes: (bufferRow, softWrapped, lineNumberNode) ->
|
||||
@domElementPool.freeDescendants(lineNumberNode)
|
||||
|
||||
{maxLineNumberDigits} = @newState
|
||||
|
||||
if softWrapped
|
||||
lineNumber = "•"
|
||||
else
|
||||
lineNumber = (bufferRow + 1).toString()
|
||||
padding = _.multiplyString("\u00a0", maxLineNumberDigits - lineNumber.length)
|
||||
|
||||
textNode = @domElementPool.buildText(padding + lineNumber)
|
||||
iconRight = @domElementPool.buildElement("div", "icon-right")
|
||||
|
||||
lineNumberNode.appendChild(textNode)
|
||||
lineNumberNode.appendChild(iconRight)
|
||||
|
||||
updateLineNumberNode: (lineNumberId, newLineNumberState) ->
|
||||
oldLineNumberState = @oldTileState.lineNumbers[lineNumberId]
|
||||
node = @lineNumberNodesById[lineNumberId]
|
||||
|
||||
unless oldLineNumberState.foldable is newLineNumberState.foldable and _.isEqual(oldLineNumberState.decorationClasses, newLineNumberState.decorationClasses)
|
||||
node.className = @buildLineNumberClassName(newLineNumberState)
|
||||
oldLineNumberState.foldable = newLineNumberState.foldable
|
||||
oldLineNumberState.decorationClasses = _.clone(newLineNumberState.decorationClasses)
|
||||
|
||||
unless oldLineNumberState.screenRow is newLineNumberState.screenRow and oldLineNumberState.bufferRow is newLineNumberState.bufferRow
|
||||
@setLineNumberInnerNodes(newLineNumberState.bufferRow, newLineNumberState.softWrapped, node)
|
||||
node.dataset.screenRow = newLineNumberState.screenRow
|
||||
node.dataset.bufferRow = newLineNumberState.bufferRow
|
||||
oldLineNumberState.screenRow = newLineNumberState.screenRow
|
||||
oldLineNumberState.bufferRow = newLineNumberState.bufferRow
|
||||
|
||||
unless oldLineNumberState.blockDecorationsHeight is newLineNumberState.blockDecorationsHeight
|
||||
node.style.marginTop = newLineNumberState.blockDecorationsHeight + "px"
|
||||
oldLineNumberState.blockDecorationsHeight = newLineNumberState.blockDecorationsHeight
|
||||
|
||||
buildLineNumberClassName: ({bufferRow, foldable, decorationClasses, softWrapped}) ->
|
||||
className = "line-number"
|
||||
className += " " + decorationClasses.join(' ') if decorationClasses?
|
||||
className += " foldable" if foldable and not softWrapped
|
||||
className
|
||||
|
||||
lineNumberNodeForScreenRow: (screenRow) ->
|
||||
for id, lineNumberState of @oldTileState.lineNumbers
|
||||
if lineNumberState.screenRow is screenRow
|
||||
return @lineNumberNodesById[id]
|
||||
null
|
||||
@@ -1,106 +0,0 @@
|
||||
CursorsComponent = require './cursors-component'
|
||||
LinesTileComponent = require './lines-tile-component'
|
||||
TiledComponent = require './tiled-component'
|
||||
|
||||
DummyLineNode = document.createElement('div')
|
||||
DummyLineNode.className = 'line'
|
||||
DummyLineNode.style.position = 'absolute'
|
||||
DummyLineNode.style.visibility = 'hidden'
|
||||
DummyLineNode.appendChild(document.createElement('span'))
|
||||
DummyLineNode.appendChild(document.createElement('span'))
|
||||
DummyLineNode.appendChild(document.createElement('span'))
|
||||
DummyLineNode.appendChild(document.createElement('span'))
|
||||
DummyLineNode.children[0].textContent = 'x'
|
||||
DummyLineNode.children[1].textContent = '我'
|
||||
DummyLineNode.children[2].textContent = 'ハ'
|
||||
DummyLineNode.children[3].textContent = '세'
|
||||
|
||||
RangeForMeasurement = document.createRange()
|
||||
|
||||
module.exports =
|
||||
class LinesComponent extends TiledComponent
|
||||
placeholderTextDiv: null
|
||||
|
||||
constructor: ({@presenter, @useShadowDOM, @domElementPool, @assert, @grammars}) ->
|
||||
@domNode = document.createElement('div')
|
||||
@domNode.classList.add('lines')
|
||||
@tilesNode = document.createElement("div")
|
||||
# Create a new stacking context, so that tiles z-index does not interfere
|
||||
# with other visual elements.
|
||||
@tilesNode.style.isolation = "isolate"
|
||||
@tilesNode.style.zIndex = 0
|
||||
@domNode.appendChild(@tilesNode)
|
||||
|
||||
@cursorsComponent = new CursorsComponent
|
||||
@domNode.appendChild(@cursorsComponent.getDomNode())
|
||||
|
||||
if @useShadowDOM
|
||||
insertionPoint = document.createElement('content')
|
||||
insertionPoint.setAttribute('select', '.overlayer')
|
||||
@domNode.appendChild(insertionPoint)
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
shouldRecreateAllTilesOnUpdate: ->
|
||||
@oldState.indentGuidesVisible isnt @newState.indentGuidesVisible or @newState.continuousReflow
|
||||
|
||||
beforeUpdateSync: (state) ->
|
||||
if @newState.maxHeight isnt @oldState.maxHeight
|
||||
@domNode.style.height = @newState.maxHeight + 'px'
|
||||
@oldState.maxHeight = @newState.maxHeight
|
||||
|
||||
if @newState.backgroundColor isnt @oldState.backgroundColor
|
||||
@domNode.style.backgroundColor = @newState.backgroundColor
|
||||
@oldState.backgroundColor = @newState.backgroundColor
|
||||
|
||||
afterUpdateSync: (state) ->
|
||||
if @newState.placeholderText isnt @oldState.placeholderText
|
||||
@placeholderTextDiv?.remove()
|
||||
if @newState.placeholderText?
|
||||
@placeholderTextDiv = document.createElement('div')
|
||||
@placeholderTextDiv.classList.add('placeholder-text')
|
||||
@placeholderTextDiv.textContent = @newState.placeholderText
|
||||
@domNode.appendChild(@placeholderTextDiv)
|
||||
@oldState.placeholderText = @newState.placeholderText
|
||||
|
||||
if @newState.width isnt @oldState.width
|
||||
@domNode.style.width = @newState.width + 'px'
|
||||
@oldState.width = @newState.width
|
||||
|
||||
@cursorsComponent.updateSync(state)
|
||||
|
||||
@oldState.indentGuidesVisible = @newState.indentGuidesVisible
|
||||
|
||||
buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter, @domElementPool, @assert, @grammars})
|
||||
|
||||
buildEmptyState: ->
|
||||
{tiles: {}}
|
||||
|
||||
getNewState: (state) ->
|
||||
state.content
|
||||
|
||||
getTilesNode: -> @tilesNode
|
||||
|
||||
measureLineHeightAndDefaultCharWidth: ->
|
||||
@domNode.appendChild(DummyLineNode)
|
||||
textNode = DummyLineNode.firstChild.childNodes[0]
|
||||
|
||||
lineHeightInPixels = DummyLineNode.getBoundingClientRect().height
|
||||
defaultCharWidth = DummyLineNode.children[0].getBoundingClientRect().width
|
||||
doubleWidthCharWidth = DummyLineNode.children[1].getBoundingClientRect().width
|
||||
halfWidthCharWidth = DummyLineNode.children[2].getBoundingClientRect().width
|
||||
koreanCharWidth = DummyLineNode.children[3].getBoundingClientRect().width
|
||||
|
||||
@domNode.removeChild(DummyLineNode)
|
||||
|
||||
@presenter.setLineHeight(lineHeightInPixels)
|
||||
@presenter.setBaseCharacterWidth(defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth)
|
||||
|
||||
lineNodeForLineIdAndScreenRow: (lineId, screenRow) ->
|
||||
tile = @presenter.tileForRow(screenRow)
|
||||
@getComponentForTile(tile)?.lineNodeForLineId(lineId)
|
||||
|
||||
textNodesForLineIdAndScreenRow: (lineId, screenRow) ->
|
||||
tile = @presenter.tileForRow(screenRow)
|
||||
@getComponentForTile(tile)?.textNodesForLineId(lineId)
|
||||
@@ -1,438 +0,0 @@
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
HighlightsComponent = require './highlights-component'
|
||||
TokenIterator = require './token-iterator'
|
||||
AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
|
||||
TokenTextEscapeRegex = /[&"'<>]/g
|
||||
MaxTokenLength = 20000
|
||||
|
||||
cloneObject = (object) ->
|
||||
clone = {}
|
||||
clone[key] = value for key, value of object
|
||||
clone
|
||||
|
||||
module.exports =
|
||||
class LinesTileComponent
|
||||
constructor: ({@presenter, @id, @domElementPool, @assert, grammars}) ->
|
||||
@tokenIterator = new TokenIterator(grammarRegistry: grammars)
|
||||
@measuredLines = new Set
|
||||
@lineNodesByLineId = {}
|
||||
@screenRowsByLineId = {}
|
||||
@lineIdsByScreenRow = {}
|
||||
@textNodesByLineId = {}
|
||||
@insertionPointsBeforeLineById = {}
|
||||
@insertionPointsAfterLineById = {}
|
||||
@domNode = @domElementPool.buildElement("div")
|
||||
@domNode.style.position = "absolute"
|
||||
@domNode.style.display = "block"
|
||||
|
||||
@highlightsComponent = new HighlightsComponent(@domElementPool)
|
||||
@domNode.appendChild(@highlightsComponent.getDomNode())
|
||||
|
||||
destroy: ->
|
||||
@domElementPool.freeElementAndDescendants(@domNode)
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
updateSync: (state) ->
|
||||
@newState = state
|
||||
unless @oldState
|
||||
@oldState = {tiles: {}}
|
||||
@oldState.tiles[@id] = {lines: {}}
|
||||
|
||||
@newTileState = @newState.tiles[@id]
|
||||
@oldTileState = @oldState.tiles[@id]
|
||||
|
||||
if @newState.backgroundColor isnt @oldState.backgroundColor
|
||||
@domNode.style.backgroundColor = @newState.backgroundColor
|
||||
@oldState.backgroundColor = @newState.backgroundColor
|
||||
|
||||
if @newTileState.zIndex isnt @oldTileState.zIndex
|
||||
@domNode.style.zIndex = @newTileState.zIndex
|
||||
@oldTileState.zIndex = @newTileState.zIndex
|
||||
|
||||
if @newTileState.display isnt @oldTileState.display
|
||||
@domNode.style.display = @newTileState.display
|
||||
@oldTileState.display = @newTileState.display
|
||||
|
||||
if @newTileState.height isnt @oldTileState.height
|
||||
@domNode.style.height = @newTileState.height + 'px'
|
||||
@oldTileState.height = @newTileState.height
|
||||
|
||||
if @newState.width isnt @oldState.width
|
||||
@domNode.style.width = @newState.width + 'px'
|
||||
@oldTileState.width = @newTileState.width
|
||||
|
||||
if @newTileState.top isnt @oldTileState.top or @newTileState.left isnt @oldTileState.left
|
||||
@domNode.style['-webkit-transform'] = "translate3d(#{@newTileState.left}px, #{@newTileState.top}px, 0px)"
|
||||
@oldTileState.top = @newTileState.top
|
||||
@oldTileState.left = @newTileState.left
|
||||
|
||||
@removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible
|
||||
@updateLineNodes()
|
||||
|
||||
@highlightsComponent.updateSync(@newTileState)
|
||||
|
||||
@oldState.indentGuidesVisible = @newState.indentGuidesVisible
|
||||
|
||||
removeLineNodes: ->
|
||||
@removeLineNode(id) for id of @oldTileState.lines
|
||||
return
|
||||
|
||||
removeLineNode: (id) ->
|
||||
@domElementPool.freeElementAndDescendants(@lineNodesByLineId[id])
|
||||
@removeBlockDecorationInsertionPointBeforeLine(id)
|
||||
@removeBlockDecorationInsertionPointAfterLine(id)
|
||||
|
||||
delete @lineNodesByLineId[id]
|
||||
delete @textNodesByLineId[id]
|
||||
delete @lineIdsByScreenRow[@screenRowsByLineId[id]]
|
||||
delete @screenRowsByLineId[id]
|
||||
delete @oldTileState.lines[id]
|
||||
|
||||
updateLineNodes: ->
|
||||
for id of @oldTileState.lines
|
||||
unless @newTileState.lines.hasOwnProperty(id)
|
||||
@removeLineNode(id)
|
||||
|
||||
newLineIds = null
|
||||
newLineNodes = null
|
||||
|
||||
for id, lineState of @newTileState.lines
|
||||
if @oldTileState.lines.hasOwnProperty(id)
|
||||
@updateLineNode(id)
|
||||
else
|
||||
newLineIds ?= []
|
||||
newLineNodes ?= []
|
||||
newLineIds.push(id)
|
||||
newLineNodes.push(@buildLineNode(id))
|
||||
@screenRowsByLineId[id] = lineState.screenRow
|
||||
@lineIdsByScreenRow[lineState.screenRow] = id
|
||||
@oldTileState.lines[id] = cloneObject(lineState)
|
||||
|
||||
return unless newLineIds?
|
||||
|
||||
for id, i in newLineIds
|
||||
lineNode = newLineNodes[i]
|
||||
@lineNodesByLineId[id] = lineNode
|
||||
if nextNode = @findNodeNextTo(lineNode)
|
||||
@domNode.insertBefore(lineNode, nextNode)
|
||||
else
|
||||
@domNode.appendChild(lineNode)
|
||||
|
||||
@insertBlockDecorationInsertionPointBeforeLine(id)
|
||||
@insertBlockDecorationInsertionPointAfterLine(id)
|
||||
|
||||
removeBlockDecorationInsertionPointBeforeLine: (id) ->
|
||||
if insertionPoint = @insertionPointsBeforeLineById[id]
|
||||
@domElementPool.freeElementAndDescendants(insertionPoint)
|
||||
delete @insertionPointsBeforeLineById[id]
|
||||
|
||||
insertBlockDecorationInsertionPointBeforeLine: (id) ->
|
||||
{hasPrecedingBlockDecorations, screenRow} = @newTileState.lines[id]
|
||||
|
||||
if hasPrecedingBlockDecorations
|
||||
lineNode = @lineNodesByLineId[id]
|
||||
insertionPoint = @domElementPool.buildElement("content")
|
||||
@domNode.insertBefore(insertionPoint, lineNode)
|
||||
@insertionPointsBeforeLineById[id] = insertionPoint
|
||||
insertionPoint.dataset.screenRow = screenRow
|
||||
@updateBlockDecorationInsertionPointBeforeLine(id)
|
||||
|
||||
updateBlockDecorationInsertionPointBeforeLine: (id) ->
|
||||
oldLineState = @oldTileState.lines[id]
|
||||
newLineState = @newTileState.lines[id]
|
||||
insertionPoint = @insertionPointsBeforeLineById[id]
|
||||
return unless insertionPoint?
|
||||
|
||||
if newLineState.screenRow isnt oldLineState.screenRow
|
||||
insertionPoint.dataset.screenRow = newLineState.screenRow
|
||||
|
||||
precedingBlockDecorationsSelector = newLineState.precedingBlockDecorations.map((d) -> "#atom--block-decoration-#{d.id}").join(',')
|
||||
|
||||
if precedingBlockDecorationsSelector isnt oldLineState.precedingBlockDecorationsSelector
|
||||
insertionPoint.setAttribute("select", precedingBlockDecorationsSelector)
|
||||
oldLineState.precedingBlockDecorationsSelector = precedingBlockDecorationsSelector
|
||||
|
||||
removeBlockDecorationInsertionPointAfterLine: (id) ->
|
||||
if insertionPoint = @insertionPointsAfterLineById[id]
|
||||
@domElementPool.freeElementAndDescendants(insertionPoint)
|
||||
delete @insertionPointsAfterLineById[id]
|
||||
|
||||
insertBlockDecorationInsertionPointAfterLine: (id) ->
|
||||
{hasFollowingBlockDecorations, screenRow} = @newTileState.lines[id]
|
||||
|
||||
if hasFollowingBlockDecorations
|
||||
lineNode = @lineNodesByLineId[id]
|
||||
insertionPoint = @domElementPool.buildElement("content")
|
||||
@domNode.insertBefore(insertionPoint, lineNode.nextSibling)
|
||||
@insertionPointsAfterLineById[id] = insertionPoint
|
||||
insertionPoint.dataset.screenRow = screenRow
|
||||
@updateBlockDecorationInsertionPointAfterLine(id)
|
||||
|
||||
updateBlockDecorationInsertionPointAfterLine: (id) ->
|
||||
oldLineState = @oldTileState.lines[id]
|
||||
newLineState = @newTileState.lines[id]
|
||||
insertionPoint = @insertionPointsAfterLineById[id]
|
||||
return unless insertionPoint?
|
||||
|
||||
if newLineState.screenRow isnt oldLineState.screenRow
|
||||
insertionPoint.dataset.screenRow = newLineState.screenRow
|
||||
|
||||
followingBlockDecorationsSelector = newLineState.followingBlockDecorations.map((d) -> "#atom--block-decoration-#{d.id}").join(',')
|
||||
|
||||
if followingBlockDecorationsSelector isnt oldLineState.followingBlockDecorationsSelector
|
||||
insertionPoint.setAttribute("select", followingBlockDecorationsSelector)
|
||||
oldLineState.followingBlockDecorationsSelector = followingBlockDecorationsSelector
|
||||
|
||||
findNodeNextTo: (node) ->
|
||||
for nextNode, index in @domNode.children
|
||||
continue if index is 0 # skips highlights node
|
||||
return nextNode if @screenRowForNode(node) < @screenRowForNode(nextNode)
|
||||
return
|
||||
|
||||
screenRowForNode: (node) -> parseInt(node.dataset.screenRow)
|
||||
|
||||
buildLineNode: (id) ->
|
||||
{width} = @newState
|
||||
{screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newTileState.lines[id]
|
||||
|
||||
lineNode = @domElementPool.buildElement("div", "line")
|
||||
lineNode.dataset.screenRow = screenRow
|
||||
|
||||
if decorationClasses?
|
||||
for decorationClass in decorationClasses
|
||||
lineNode.classList.add(decorationClass)
|
||||
|
||||
@currentLineTextNodes = []
|
||||
if text is ""
|
||||
@setEmptyLineInnerNodes(id, lineNode)
|
||||
else
|
||||
@setLineInnerNodes(id, lineNode)
|
||||
@textNodesByLineId[id] = @currentLineTextNodes
|
||||
|
||||
lineNode.appendChild(@domElementPool.buildElement("span", "fold-marker")) if fold
|
||||
lineNode
|
||||
|
||||
setEmptyLineInnerNodes: (id, lineNode) ->
|
||||
{indentGuidesVisible} = @newState
|
||||
{indentLevel, tabLength, endOfLineInvisibles} = @newTileState.lines[id]
|
||||
|
||||
if indentGuidesVisible and indentLevel > 0
|
||||
invisibleIndex = 0
|
||||
for i in [0...indentLevel]
|
||||
indentGuide = @domElementPool.buildElement("span", "indent-guide")
|
||||
for j in [0...tabLength]
|
||||
if invisible = endOfLineInvisibles?[invisibleIndex++]
|
||||
invisibleSpan = @domElementPool.buildElement("span", "invisible-character")
|
||||
textNode = @domElementPool.buildText(invisible)
|
||||
invisibleSpan.appendChild(textNode)
|
||||
indentGuide.appendChild(invisibleSpan)
|
||||
|
||||
@currentLineTextNodes.push(textNode)
|
||||
else
|
||||
textNode = @domElementPool.buildText(" ")
|
||||
indentGuide.appendChild(textNode)
|
||||
|
||||
@currentLineTextNodes.push(textNode)
|
||||
lineNode.appendChild(indentGuide)
|
||||
|
||||
while invisibleIndex < endOfLineInvisibles?.length
|
||||
invisible = endOfLineInvisibles[invisibleIndex++]
|
||||
invisibleSpan = @domElementPool.buildElement("span", "invisible-character")
|
||||
textNode = @domElementPool.buildText(invisible)
|
||||
invisibleSpan.appendChild(textNode)
|
||||
lineNode.appendChild(invisibleSpan)
|
||||
|
||||
@currentLineTextNodes.push(textNode)
|
||||
else
|
||||
unless @appendEndOfLineNodes(id, lineNode)
|
||||
textNode = @domElementPool.buildText("\u00a0")
|
||||
lineNode.appendChild(textNode)
|
||||
|
||||
@currentLineTextNodes.push(textNode)
|
||||
|
||||
setLineInnerNodes: (id, lineNode) ->
|
||||
lineState = @newTileState.lines[id]
|
||||
{firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, invisibles} = lineState
|
||||
lineIsWhitespaceOnly = firstTrailingWhitespaceIndex is 0
|
||||
|
||||
@tokenIterator.reset(lineState)
|
||||
openScopeNode = lineNode
|
||||
|
||||
while @tokenIterator.next()
|
||||
for scope in @tokenIterator.getScopeEnds()
|
||||
openScopeNode = openScopeNode.parentElement
|
||||
|
||||
for scope in @tokenIterator.getScopeStarts()
|
||||
newScopeNode = @domElementPool.buildElement("span", scope.replace(/\.+/g, ' '))
|
||||
openScopeNode.appendChild(newScopeNode)
|
||||
openScopeNode = newScopeNode
|
||||
|
||||
tokenStart = @tokenIterator.getScreenStart()
|
||||
tokenEnd = @tokenIterator.getScreenEnd()
|
||||
tokenText = @tokenIterator.getText()
|
||||
isHardTab = @tokenIterator.isHardTab()
|
||||
|
||||
if hasLeadingWhitespace = tokenStart < firstNonWhitespaceIndex
|
||||
tokenFirstNonWhitespaceIndex = firstNonWhitespaceIndex - tokenStart
|
||||
else
|
||||
tokenFirstNonWhitespaceIndex = null
|
||||
|
||||
if hasTrailingWhitespace = tokenEnd > firstTrailingWhitespaceIndex
|
||||
tokenFirstTrailingWhitespaceIndex = Math.max(0, firstTrailingWhitespaceIndex - tokenStart)
|
||||
else
|
||||
tokenFirstTrailingWhitespaceIndex = null
|
||||
|
||||
hasIndentGuide =
|
||||
@newState.indentGuidesVisible and
|
||||
(hasLeadingWhitespace or lineIsWhitespaceOnly)
|
||||
|
||||
hasInvisibleCharacters =
|
||||
(invisibles?.tab and isHardTab) or
|
||||
(invisibles?.space and (hasLeadingWhitespace or hasTrailingWhitespace))
|
||||
|
||||
@appendTokenNodes(tokenText, isHardTab, tokenFirstNonWhitespaceIndex, tokenFirstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, openScopeNode)
|
||||
|
||||
@appendEndOfLineNodes(id, lineNode)
|
||||
|
||||
appendTokenNodes: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, scopeNode) ->
|
||||
if isHardTab
|
||||
textNode = @domElementPool.buildText(tokenText)
|
||||
hardTabNode = @domElementPool.buildElement("span", "hard-tab")
|
||||
hardTabNode.classList.add("leading-whitespace") if firstNonWhitespaceIndex?
|
||||
hardTabNode.classList.add("trailing-whitespace") if firstTrailingWhitespaceIndex?
|
||||
hardTabNode.classList.add("indent-guide") if hasIndentGuide
|
||||
hardTabNode.classList.add("invisible-character") if hasInvisibleCharacters
|
||||
hardTabNode.appendChild(textNode)
|
||||
|
||||
scopeNode.appendChild(hardTabNode)
|
||||
@currentLineTextNodes.push(textNode)
|
||||
else
|
||||
startIndex = 0
|
||||
endIndex = tokenText.length
|
||||
|
||||
leadingWhitespaceNode = null
|
||||
leadingWhitespaceTextNode = null
|
||||
trailingWhitespaceNode = null
|
||||
trailingWhitespaceTextNode = null
|
||||
|
||||
if firstNonWhitespaceIndex?
|
||||
leadingWhitespaceTextNode =
|
||||
@domElementPool.buildText(tokenText.substring(0, firstNonWhitespaceIndex))
|
||||
leadingWhitespaceNode = @domElementPool.buildElement("span", "leading-whitespace")
|
||||
leadingWhitespaceNode.classList.add("indent-guide") if hasIndentGuide
|
||||
leadingWhitespaceNode.classList.add("invisible-character") if hasInvisibleCharacters
|
||||
leadingWhitespaceNode.appendChild(leadingWhitespaceTextNode)
|
||||
|
||||
startIndex = firstNonWhitespaceIndex
|
||||
|
||||
if firstTrailingWhitespaceIndex?
|
||||
tokenIsOnlyWhitespace = firstTrailingWhitespaceIndex is 0
|
||||
|
||||
trailingWhitespaceTextNode =
|
||||
@domElementPool.buildText(tokenText.substring(firstTrailingWhitespaceIndex))
|
||||
trailingWhitespaceNode = @domElementPool.buildElement("span", "trailing-whitespace")
|
||||
trailingWhitespaceNode.classList.add("indent-guide") if hasIndentGuide and not firstNonWhitespaceIndex? and tokenIsOnlyWhitespace
|
||||
trailingWhitespaceNode.classList.add("invisible-character") if hasInvisibleCharacters
|
||||
trailingWhitespaceNode.appendChild(trailingWhitespaceTextNode)
|
||||
|
||||
endIndex = firstTrailingWhitespaceIndex
|
||||
|
||||
if leadingWhitespaceNode?
|
||||
scopeNode.appendChild(leadingWhitespaceNode)
|
||||
@currentLineTextNodes.push(leadingWhitespaceTextNode)
|
||||
|
||||
if tokenText.length > MaxTokenLength
|
||||
while startIndex < endIndex
|
||||
textNode = @domElementPool.buildText(
|
||||
@sliceText(tokenText, startIndex, startIndex + MaxTokenLength)
|
||||
)
|
||||
textSpan = @domElementPool.buildElement("span")
|
||||
|
||||
textSpan.appendChild(textNode)
|
||||
scopeNode.appendChild(textSpan)
|
||||
startIndex += MaxTokenLength
|
||||
@currentLineTextNodes.push(textNode)
|
||||
else
|
||||
textNode = @domElementPool.buildText(@sliceText(tokenText, startIndex, endIndex))
|
||||
scopeNode.appendChild(textNode)
|
||||
@currentLineTextNodes.push(textNode)
|
||||
|
||||
if trailingWhitespaceNode?
|
||||
scopeNode.appendChild(trailingWhitespaceNode)
|
||||
@currentLineTextNodes.push(trailingWhitespaceTextNode)
|
||||
|
||||
sliceText: (tokenText, startIndex, endIndex) ->
|
||||
if startIndex? and endIndex? and startIndex > 0 or endIndex < tokenText.length
|
||||
tokenText = tokenText.slice(startIndex, endIndex)
|
||||
tokenText
|
||||
|
||||
appendEndOfLineNodes: (id, lineNode) ->
|
||||
{endOfLineInvisibles} = @newTileState.lines[id]
|
||||
|
||||
hasInvisibles = false
|
||||
if endOfLineInvisibles?
|
||||
for invisible in endOfLineInvisibles
|
||||
hasInvisibles = true
|
||||
invisibleSpan = @domElementPool.buildElement("span", "invisible-character")
|
||||
textNode = @domElementPool.buildText(invisible)
|
||||
invisibleSpan.appendChild(textNode)
|
||||
lineNode.appendChild(invisibleSpan)
|
||||
|
||||
@currentLineTextNodes.push(textNode)
|
||||
|
||||
hasInvisibles
|
||||
|
||||
updateLineNode: (id) ->
|
||||
oldLineState = @oldTileState.lines[id]
|
||||
newLineState = @newTileState.lines[id]
|
||||
|
||||
lineNode = @lineNodesByLineId[id]
|
||||
|
||||
newDecorationClasses = newLineState.decorationClasses
|
||||
oldDecorationClasses = oldLineState.decorationClasses
|
||||
|
||||
if oldDecorationClasses?
|
||||
for decorationClass in oldDecorationClasses
|
||||
unless newDecorationClasses? and decorationClass in newDecorationClasses
|
||||
lineNode.classList.remove(decorationClass)
|
||||
|
||||
if newDecorationClasses?
|
||||
for decorationClass in newDecorationClasses
|
||||
unless oldDecorationClasses? and decorationClass in oldDecorationClasses
|
||||
lineNode.classList.add(decorationClass)
|
||||
|
||||
oldLineState.decorationClasses = newLineState.decorationClasses
|
||||
|
||||
if not oldLineState.hasPrecedingBlockDecorations and newLineState.hasPrecedingBlockDecorations
|
||||
@insertBlockDecorationInsertionPointBeforeLine(id)
|
||||
else if oldLineState.hasPrecedingBlockDecorations and not newLineState.hasPrecedingBlockDecorations
|
||||
@removeBlockDecorationInsertionPointBeforeLine(id)
|
||||
|
||||
if not oldLineState.hasFollowingBlockDecorations and newLineState.hasFollowingBlockDecorations
|
||||
@insertBlockDecorationInsertionPointAfterLine(id)
|
||||
else if oldLineState.hasFollowingBlockDecorations and not newLineState.hasFollowingBlockDecorations
|
||||
@removeBlockDecorationInsertionPointAfterLine(id)
|
||||
|
||||
if newLineState.screenRow isnt oldLineState.screenRow
|
||||
lineNode.dataset.screenRow = newLineState.screenRow
|
||||
@lineIdsByScreenRow[newLineState.screenRow] = id
|
||||
@screenRowsByLineId[id] = newLineState.screenRow
|
||||
|
||||
@updateBlockDecorationInsertionPointBeforeLine(id)
|
||||
@updateBlockDecorationInsertionPointAfterLine(id)
|
||||
|
||||
oldLineState.screenRow = newLineState.screenRow
|
||||
oldLineState.hasPrecedingBlockDecorations = newLineState.hasPrecedingBlockDecorations
|
||||
oldLineState.hasFollowingBlockDecorations = newLineState.hasFollowingBlockDecorations
|
||||
|
||||
lineNodeForScreenRow: (screenRow) ->
|
||||
@lineNodesByLineId[@lineIdsByScreenRow[screenRow]]
|
||||
|
||||
lineNodeForLineId: (lineId) ->
|
||||
@lineNodesByLineId[lineId]
|
||||
|
||||
textNodesForLineId: (lineId) ->
|
||||
@textNodesByLineId[lineId].slice()
|
||||
@@ -1,162 +0,0 @@
|
||||
TokenIterator = require './token-iterator'
|
||||
{Point} = require 'text-buffer'
|
||||
|
||||
module.exports =
|
||||
class LinesYardstick
|
||||
constructor: (@model, @lineNodesProvider, @lineTopIndex, grammarRegistry) ->
|
||||
@tokenIterator = new TokenIterator({grammarRegistry})
|
||||
@rangeForMeasurement = document.createRange()
|
||||
@invalidateCache()
|
||||
|
||||
invalidateCache: ->
|
||||
@pixelPositionsByLineIdAndColumn = {}
|
||||
|
||||
measuredRowForPixelPosition: (pixelPosition) ->
|
||||
targetTop = pixelPosition.top
|
||||
row = Math.floor(targetTop / @model.getLineHeightInPixels())
|
||||
row if 0 <= row <= @model.getLastScreenRow()
|
||||
|
||||
screenPositionForPixelPosition: (pixelPosition) ->
|
||||
targetTop = pixelPosition.top
|
||||
targetLeft = pixelPosition.left
|
||||
defaultCharWidth = @model.getDefaultCharWidth()
|
||||
row = @lineTopIndex.rowForPixelPosition(targetTop)
|
||||
targetLeft = 0 if targetTop < 0
|
||||
targetLeft = Infinity if row > @model.getLastScreenRow()
|
||||
row = Math.min(row, @model.getLastScreenRow())
|
||||
row = Math.max(0, row)
|
||||
|
||||
line = @model.tokenizedLineForScreenRow(row)
|
||||
lineNode = @lineNodesProvider.lineNodeForLineIdAndScreenRow(line?.id, row)
|
||||
|
||||
return Point(row, 0) unless lineNode? and line?
|
||||
|
||||
textNodes = @lineNodesProvider.textNodesForLineIdAndScreenRow(line.id, row)
|
||||
column = 0
|
||||
previousColumn = 0
|
||||
previousLeft = 0
|
||||
|
||||
@tokenIterator.reset(line, false)
|
||||
while @tokenIterator.next()
|
||||
text = @tokenIterator.getText()
|
||||
textIndex = 0
|
||||
while textIndex < text.length
|
||||
if @tokenIterator.isPairedCharacter()
|
||||
char = text
|
||||
charLength = 2
|
||||
textIndex += 2
|
||||
else
|
||||
char = text[textIndex]
|
||||
charLength = 1
|
||||
textIndex++
|
||||
|
||||
unless textNode?
|
||||
textNode = textNodes.shift()
|
||||
textNodeLength = textNode.textContent.length
|
||||
textNodeIndex = 0
|
||||
nextTextNodeIndex = textNodeLength
|
||||
|
||||
while nextTextNodeIndex <= column
|
||||
textNode = textNodes.shift()
|
||||
textNodeLength = textNode.textContent.length
|
||||
textNodeIndex = nextTextNodeIndex
|
||||
nextTextNodeIndex = textNodeIndex + textNodeLength
|
||||
|
||||
indexWithinTextNode = column - textNodeIndex
|
||||
left = @leftPixelPositionForCharInTextNode(lineNode, textNode, indexWithinTextNode)
|
||||
charWidth = left - previousLeft
|
||||
|
||||
return Point(row, previousColumn) if targetLeft <= previousLeft + (charWidth / 2)
|
||||
|
||||
previousLeft = left
|
||||
previousColumn = column
|
||||
column += charLength
|
||||
|
||||
if targetLeft <= previousLeft + (charWidth / 2)
|
||||
Point(row, previousColumn)
|
||||
else
|
||||
Point(row, column)
|
||||
|
||||
pixelPositionForScreenPosition: (screenPosition) ->
|
||||
targetRow = screenPosition.row
|
||||
targetColumn = screenPosition.column
|
||||
|
||||
top = @lineTopIndex.pixelPositionAfterBlocksForRow(targetRow)
|
||||
left = @leftPixelPositionForScreenPosition(targetRow, targetColumn)
|
||||
|
||||
{top, left}
|
||||
|
||||
leftPixelPositionForScreenPosition: (row, column) ->
|
||||
line = @model.tokenizedLineForScreenRow(row)
|
||||
lineNode = @lineNodesProvider.lineNodeForLineIdAndScreenRow(line?.id, row)
|
||||
|
||||
return 0 unless line? and lineNode?
|
||||
|
||||
if cachedPosition = @pixelPositionsByLineIdAndColumn[line.id]?[column]
|
||||
return cachedPosition
|
||||
|
||||
textNodes = @lineNodesProvider.textNodesForLineIdAndScreenRow(line.id, row)
|
||||
indexWithinTextNode = null
|
||||
charIndex = 0
|
||||
|
||||
@tokenIterator.reset(line, false)
|
||||
while @tokenIterator.next()
|
||||
break if foundIndexWithinTextNode?
|
||||
|
||||
text = @tokenIterator.getText()
|
||||
|
||||
textIndex = 0
|
||||
while textIndex < text.length
|
||||
if @tokenIterator.isPairedCharacter()
|
||||
char = text
|
||||
charLength = 2
|
||||
textIndex += 2
|
||||
else
|
||||
char = text[textIndex]
|
||||
charLength = 1
|
||||
textIndex++
|
||||
|
||||
unless textNode?
|
||||
textNode = textNodes.shift()
|
||||
textNodeLength = textNode.textContent.length
|
||||
textNodeIndex = 0
|
||||
nextTextNodeIndex = textNodeLength
|
||||
|
||||
while nextTextNodeIndex <= charIndex
|
||||
textNode = textNodes.shift()
|
||||
textNodeLength = textNode.textContent.length
|
||||
textNodeIndex = nextTextNodeIndex
|
||||
nextTextNodeIndex = textNodeIndex + textNodeLength
|
||||
|
||||
if charIndex is column
|
||||
foundIndexWithinTextNode = charIndex - textNodeIndex
|
||||
break
|
||||
|
||||
charIndex += charLength
|
||||
|
||||
if textNode?
|
||||
foundIndexWithinTextNode ?= textNode.textContent.length
|
||||
position = @leftPixelPositionForCharInTextNode(
|
||||
lineNode, textNode, foundIndexWithinTextNode
|
||||
)
|
||||
@pixelPositionsByLineIdAndColumn[line.id] ?= {}
|
||||
@pixelPositionsByLineIdAndColumn[line.id][column] = position
|
||||
position
|
||||
else
|
||||
0
|
||||
|
||||
leftPixelPositionForCharInTextNode: (lineNode, textNode, charIndex) ->
|
||||
if charIndex is 0
|
||||
width = 0
|
||||
else
|
||||
@rangeForMeasurement.setStart(textNode, 0)
|
||||
@rangeForMeasurement.setEnd(textNode, charIndex)
|
||||
width = @rangeForMeasurement.getBoundingClientRect().width
|
||||
|
||||
@rangeForMeasurement.setStart(textNode, 0)
|
||||
@rangeForMeasurement.setEnd(textNode, textNode.textContent.length)
|
||||
left = @rangeForMeasurement.getBoundingClientRect().left
|
||||
|
||||
offset = lineNode.getBoundingClientRect().left
|
||||
|
||||
left + width - offset
|
||||
225
src/main-process/application-menu.js
Normal file
225
src/main-process/application-menu.js
Normal file
@@ -0,0 +1,225 @@
|
||||
const {app, Menu} = require('electron')
|
||||
const _ = require('underscore-plus')
|
||||
const MenuHelpers = require('../menu-helpers')
|
||||
|
||||
// Used to manage the global application menu.
|
||||
//
|
||||
// It's created by {AtomApplication} upon instantiation and used to add, remove
|
||||
// and maintain the state of all menu items.
|
||||
module.exports =
|
||||
class ApplicationMenu {
|
||||
constructor (version, autoUpdateManager) {
|
||||
this.version = version
|
||||
this.autoUpdateManager = autoUpdateManager
|
||||
this.windowTemplates = new WeakMap()
|
||||
this.setActiveTemplate(this.getDefaultTemplate())
|
||||
this.autoUpdateManager.on('state-changed', state => this.showUpdateMenuItem(state))
|
||||
}
|
||||
|
||||
// Public: Updates the entire menu with the given keybindings.
|
||||
//
|
||||
// window - The BrowserWindow this menu template is associated with.
|
||||
// template - The Object which describes the menu to display.
|
||||
// keystrokesByCommand - An Object where the keys are commands and the values
|
||||
// are Arrays containing the keystroke.
|
||||
update (window, template, keystrokesByCommand) {
|
||||
this.translateTemplate(template, keystrokesByCommand)
|
||||
this.substituteVersion(template)
|
||||
this.windowTemplates.set(window, template)
|
||||
if (window === this.lastFocusedWindow) return this.setActiveTemplate(template)
|
||||
}
|
||||
|
||||
setActiveTemplate (template) {
|
||||
if (!_.isEqual(template, this.activeTemplate)) {
|
||||
this.activeTemplate = template
|
||||
this.menu = Menu.buildFromTemplate(_.deepClone(template))
|
||||
Menu.setApplicationMenu(this.menu)
|
||||
}
|
||||
|
||||
return this.showUpdateMenuItem(this.autoUpdateManager.getState())
|
||||
}
|
||||
|
||||
// Register a BrowserWindow with this application menu.
|
||||
addWindow (window) {
|
||||
if (this.lastFocusedWindow == null) this.lastFocusedWindow = window
|
||||
|
||||
const focusHandler = () => {
|
||||
this.lastFocusedWindow = window
|
||||
const template = this.windowTemplates.get(window)
|
||||
if (template) this.setActiveTemplate(template)
|
||||
}
|
||||
|
||||
window.on('focus', focusHandler)
|
||||
window.once('closed', () => {
|
||||
if (window === this.lastFocusedWindow) this.lastFocusedWindow = null
|
||||
this.windowTemplates.delete(window)
|
||||
window.removeListener('focus', focusHandler)
|
||||
})
|
||||
|
||||
this.enableWindowSpecificItems(true)
|
||||
}
|
||||
|
||||
// Flattens the given menu and submenu items into an single Array.
|
||||
//
|
||||
// menu - A complete menu configuration object for atom-shell's menu API.
|
||||
//
|
||||
// Returns an Array of native menu items.
|
||||
flattenMenuItems (menu) {
|
||||
const object = menu.items || {}
|
||||
let items = []
|
||||
for (let index in object) {
|
||||
const item = object[index]
|
||||
items.push(item)
|
||||
if (item.submenu) items = items.concat(this.flattenMenuItems(item.submenu))
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// Flattens the given menu template into an single Array.
|
||||
//
|
||||
// template - An object describing the menu item.
|
||||
//
|
||||
// Returns an Array of native menu items.
|
||||
flattenMenuTemplate (template) {
|
||||
let items = []
|
||||
for (let item of template) {
|
||||
items.push(item)
|
||||
if (item.submenu) items = items.concat(this.flattenMenuTemplate(item.submenu))
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// Public: Used to make all window related menu items are active.
|
||||
//
|
||||
// enable - If true enables all window specific items, if false disables all
|
||||
// window specific items.
|
||||
enableWindowSpecificItems (enable) {
|
||||
for (let item of this.flattenMenuItems(this.menu)) {
|
||||
if (item.metadata && item.metadata.windowSpecific) item.enabled = enable
|
||||
}
|
||||
}
|
||||
|
||||
// Replaces VERSION with the current version.
|
||||
substituteVersion (template) {
|
||||
let item = this.flattenMenuTemplate(template).find(({label}) => label === 'VERSION')
|
||||
if (item) item.label = `Version ${this.version}`
|
||||
}
|
||||
|
||||
// Sets the proper visible state the update menu items
|
||||
showUpdateMenuItem (state) {
|
||||
const items = this.flattenMenuItems(this.menu)
|
||||
const checkForUpdateItem = items.find(({label}) => label === 'Check for Update')
|
||||
const checkingForUpdateItem = items.find(({label}) => label === 'Checking for Update')
|
||||
const downloadingUpdateItem = items.find(({label}) => label === 'Downloading Update')
|
||||
const installUpdateItem = items.find(({label}) => label === 'Restart and Install Update')
|
||||
|
||||
if (!checkForUpdateItem || !checkingForUpdateItem ||
|
||||
!downloadingUpdateItem || !installUpdateItem) return
|
||||
|
||||
checkForUpdateItem.visible = false
|
||||
checkingForUpdateItem.visible = false
|
||||
downloadingUpdateItem.visible = false
|
||||
installUpdateItem.visible = false
|
||||
|
||||
switch (state) {
|
||||
case 'idle':
|
||||
case 'error':
|
||||
case 'no-update-available':
|
||||
checkForUpdateItem.visible = true
|
||||
break
|
||||
case 'checking':
|
||||
checkingForUpdateItem.visible = true
|
||||
break
|
||||
case 'downloading':
|
||||
downloadingUpdateItem.visible = true
|
||||
break
|
||||
case 'update-available':
|
||||
installUpdateItem.visible = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Default list of menu items.
|
||||
//
|
||||
// Returns an Array of menu item Objects.
|
||||
getDefaultTemplate () {
|
||||
return [{
|
||||
label: 'Atom',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Check for Update',
|
||||
metadata: {autoUpdate: true}
|
||||
},
|
||||
{
|
||||
label: 'Reload',
|
||||
accelerator: 'Command+R',
|
||||
click: () => {
|
||||
const window = this.focusedWindow()
|
||||
if (window) window.reload()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Close Window',
|
||||
accelerator: 'Command+Shift+W',
|
||||
click: () => {
|
||||
const window = this.focusedWindow()
|
||||
if (window) window.close()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Toggle Dev Tools',
|
||||
accelerator: 'Command+Alt+I',
|
||||
click: () => {
|
||||
const window = this.focusedWindow()
|
||||
if (window) window.toggleDevTools()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Quit',
|
||||
accelerator: 'Command+Q',
|
||||
click: () => app.quit()
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
|
||||
focusedWindow () {
|
||||
return global.atomApplication.getAllWindows().find(window => window.isFocused())
|
||||
}
|
||||
|
||||
// Combines a menu template with the appropriate keystroke.
|
||||
//
|
||||
// template - An Object conforming to atom-shell's menu api but lacking
|
||||
// accelerator and click properties.
|
||||
// keystrokesByCommand - An Object where the keys are commands and the values
|
||||
// are Arrays containing the keystroke.
|
||||
//
|
||||
// Returns a complete menu configuration object for atom-shell's menu API.
|
||||
translateTemplate (template, keystrokesByCommand) {
|
||||
template.forEach(item => {
|
||||
if (item.metadata == null) item.metadata = {}
|
||||
if (item.command) {
|
||||
item.accelerator = this.acceleratorForCommand(item.command, keystrokesByCommand)
|
||||
item.click = () => global.atomApplication.sendCommand(item.command, item.commandDetail)
|
||||
if (!/^application:/.test(item.command)) {
|
||||
item.metadata.windowSpecific = true
|
||||
}
|
||||
}
|
||||
if (item.submenu) this.translateTemplate(item.submenu, keystrokesByCommand)
|
||||
})
|
||||
return template
|
||||
}
|
||||
|
||||
// Determine the accelerator for a given command.
|
||||
//
|
||||
// command - The name of the command.
|
||||
// keystrokesByCommand - An Object where the keys are commands and the values
|
||||
// are Arrays containing the keystroke.
|
||||
//
|
||||
// Returns a String containing the keystroke in a format that can be interpreted
|
||||
// by Electron to provide nice icons where available.
|
||||
acceleratorForCommand (command, keystrokesByCommand) {
|
||||
const firstKeystroke = keystrokesByCommand[command] && keystrokesByCommand[command][0]
|
||||
return MenuHelpers.acceleratorForKeystroke(firstKeystroke)
|
||||
}
|
||||
}
|
||||
1425
src/main-process/atom-application.js
Normal file
1425
src/main-process/atom-application.js
Normal file
File diff suppressed because it is too large
Load Diff
55
src/main-process/atom-protocol-handler.js
Normal file
55
src/main-process/atom-protocol-handler.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const {protocol} = require('electron')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
// Handles requests with 'atom' protocol.
|
||||
//
|
||||
// It's created by {AtomApplication} upon instantiation and is used to create a
|
||||
// custom resource loader for 'atom://' URLs.
|
||||
//
|
||||
// The following directories are searched in order:
|
||||
// * ~/.atom/assets
|
||||
// * ~/.atom/dev/packages (unless in safe mode)
|
||||
// * ~/.atom/packages
|
||||
// * RESOURCE_PATH/node_modules
|
||||
//
|
||||
module.exports =
|
||||
class AtomProtocolHandler {
|
||||
constructor (resourcePath, safeMode) {
|
||||
this.loadPaths = []
|
||||
|
||||
if (!safeMode) {
|
||||
this.loadPaths.push(path.join(process.env.ATOM_HOME, 'dev', 'packages'))
|
||||
this.loadPaths.push(path.join(resourcePath, 'packages'))
|
||||
}
|
||||
|
||||
this.loadPaths.push(path.join(process.env.ATOM_HOME, 'packages'))
|
||||
this.loadPaths.push(path.join(resourcePath, 'node_modules'))
|
||||
|
||||
this.registerAtomProtocol()
|
||||
}
|
||||
|
||||
// Creates the 'atom' custom protocol handler.
|
||||
registerAtomProtocol () {
|
||||
protocol.registerFileProtocol('atom', (request, callback) => {
|
||||
const relativePath = path.normalize(request.url.substr(7))
|
||||
|
||||
let filePath
|
||||
if (relativePath.indexOf('assets/') === 0) {
|
||||
const assetsPath = path.join(process.env.ATOM_HOME, relativePath)
|
||||
const stat = fs.statSyncNoException(assetsPath)
|
||||
if (stat && stat.isFile()) filePath = assetsPath
|
||||
}
|
||||
|
||||
if (!filePath) {
|
||||
for (let loadPath of this.loadPaths) {
|
||||
filePath = path.join(loadPath, relativePath)
|
||||
const stat = fs.statSyncNoException(filePath)
|
||||
if (stat && stat.isFile()) break
|
||||
}
|
||||
}
|
||||
|
||||
callback(filePath)
|
||||
})
|
||||
}
|
||||
}
|
||||
447
src/main-process/atom-window.js
Normal file
447
src/main-process/atom-window.js
Normal file
@@ -0,0 +1,447 @@
|
||||
const {BrowserWindow, app, dialog, ipcMain} = require('electron')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const url = require('url')
|
||||
const {EventEmitter} = require('events')
|
||||
|
||||
const ICON_PATH = path.resolve(__dirname, '..', '..', 'resources', 'atom.png')
|
||||
|
||||
let includeShellLoadTime = true
|
||||
let nextId = 0
|
||||
|
||||
module.exports =
|
||||
class AtomWindow extends EventEmitter {
|
||||
constructor (atomApplication, fileRecoveryService, settings = {}) {
|
||||
super()
|
||||
|
||||
this.id = nextId++
|
||||
this.atomApplication = atomApplication
|
||||
this.fileRecoveryService = fileRecoveryService
|
||||
this.isSpec = settings.isSpec
|
||||
this.headless = settings.headless
|
||||
this.safeMode = settings.safeMode
|
||||
this.devMode = settings.devMode
|
||||
this.resourcePath = settings.resourcePath
|
||||
|
||||
let {pathToOpen, locationsToOpen} = settings
|
||||
if (!locationsToOpen && pathToOpen) locationsToOpen = [{pathToOpen}]
|
||||
if (!locationsToOpen) locationsToOpen = []
|
||||
|
||||
this.loadedPromise = new Promise(resolve => { this.resolveLoadedPromise = resolve })
|
||||
this.closedPromise = new Promise(resolve => { this.resolveClosedPromise = resolve })
|
||||
|
||||
const options = {
|
||||
show: false,
|
||||
title: 'Atom',
|
||||
tabbingIdentifier: 'atom',
|
||||
webPreferences: {
|
||||
// Prevent specs from throttling when the window is in the background:
|
||||
// this should result in faster CI builds, and an improvement in the
|
||||
// local development experience when running specs through the UI (which
|
||||
// now won't pause when e.g. minimizing the window).
|
||||
backgroundThrottling: !this.isSpec,
|
||||
// Disable the `auxclick` feature so that `click` events are triggered in
|
||||
// response to a middle-click.
|
||||
// (Ref: https://github.com/atom/atom/pull/12696#issuecomment-290496960)
|
||||
disableBlinkFeatures: 'Auxclick'
|
||||
}
|
||||
}
|
||||
|
||||
// Don't set icon on Windows so the exe's ico will be used as window and
|
||||
// taskbar's icon. See https://github.com/atom/atom/issues/4811 for more.
|
||||
if (process.platform === 'linux') options.icon = ICON_PATH
|
||||
if (this.shouldAddCustomTitleBar()) options.titleBarStyle = 'hidden'
|
||||
if (this.shouldAddCustomInsetTitleBar()) options.titleBarStyle = 'hiddenInset'
|
||||
if (this.shouldHideTitleBar()) options.frame = false
|
||||
this.browserWindow = new BrowserWindow(options)
|
||||
|
||||
Object.defineProperty(this.browserWindow, 'loadSettingsJSON', {
|
||||
get: () => JSON.stringify(Object.assign({
|
||||
userSettings: !this.isSpec
|
||||
? this.atomApplication.configFile.get()
|
||||
: null
|
||||
}, this.loadSettings))
|
||||
})
|
||||
|
||||
this.handleEvents()
|
||||
|
||||
this.loadSettings = Object.assign({}, settings)
|
||||
this.loadSettings.appVersion = app.getVersion()
|
||||
this.loadSettings.resourcePath = this.resourcePath
|
||||
this.loadSettings.atomHome = process.env.ATOM_HOME
|
||||
if (this.loadSettings.devMode == null) this.loadSettings.devMode = false
|
||||
if (this.loadSettings.safeMode == null) this.loadSettings.safeMode = false
|
||||
if (this.loadSettings.clearWindowState == null) this.loadSettings.clearWindowState = false
|
||||
|
||||
if (!this.loadSettings.initialPaths) {
|
||||
this.loadSettings.initialPaths = []
|
||||
for (const {pathToOpen, stat} of locationsToOpen) {
|
||||
if (!pathToOpen) continue
|
||||
if (stat && stat.isDirectory()) {
|
||||
this.loadSettings.initialPaths.push(pathToOpen)
|
||||
} else {
|
||||
const parentDirectory = path.dirname(pathToOpen)
|
||||
if (stat && stat.isFile() || fs.existsSync(parentDirectory)) {
|
||||
this.loadSettings.initialPaths.push(parentDirectory)
|
||||
} else {
|
||||
this.loadSettings.initialPaths.push(pathToOpen)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.loadSettings.initialPaths.sort()
|
||||
|
||||
// Only send to the first non-spec window created
|
||||
if (includeShellLoadTime && !this.isSpec) {
|
||||
includeShellLoadTime = false
|
||||
if (!this.loadSettings.shellLoadTime) {
|
||||
this.loadSettings.shellLoadTime = Date.now() - global.shellStartTime
|
||||
}
|
||||
}
|
||||
|
||||
this.representedDirectoryPaths = this.loadSettings.initialPaths
|
||||
if (!this.loadSettings.env) this.env = this.loadSettings.env
|
||||
|
||||
this.browserWindow.on('window:loaded', () => {
|
||||
this.disableZoom()
|
||||
this.emit('window:loaded')
|
||||
this.resolveLoadedPromise()
|
||||
})
|
||||
|
||||
this.browserWindow.on('window:locations-opened', () => {
|
||||
this.emit('window:locations-opened')
|
||||
})
|
||||
|
||||
this.browserWindow.on('enter-full-screen', () => {
|
||||
this.browserWindow.webContents.send('did-enter-full-screen')
|
||||
})
|
||||
|
||||
this.browserWindow.on('leave-full-screen', () => {
|
||||
this.browserWindow.webContents.send('did-leave-full-screen')
|
||||
})
|
||||
|
||||
this.browserWindow.loadURL(
|
||||
url.format({
|
||||
protocol: 'file',
|
||||
pathname: `${this.resourcePath}/static/index.html`,
|
||||
slashes: true
|
||||
})
|
||||
)
|
||||
|
||||
this.browserWindow.showSaveDialog = this.showSaveDialog.bind(this)
|
||||
|
||||
if (this.isSpec) this.browserWindow.focusOnWebView()
|
||||
|
||||
const hasPathToOpen = !(locationsToOpen.length === 1 && locationsToOpen[0].pathToOpen == null)
|
||||
if (hasPathToOpen && !this.isSpecWindow()) this.openLocations(locationsToOpen)
|
||||
}
|
||||
|
||||
hasProjectPath () {
|
||||
return this.representedDirectoryPaths.length > 0
|
||||
}
|
||||
|
||||
setupContextMenu () {
|
||||
const ContextMenu = require('./context-menu')
|
||||
|
||||
this.browserWindow.on('context-menu', menuTemplate => {
|
||||
return new ContextMenu(menuTemplate, this)
|
||||
})
|
||||
}
|
||||
|
||||
containsPaths (paths) {
|
||||
return paths.every(p => this.containsPath(p))
|
||||
}
|
||||
|
||||
containsPath (pathToCheck) {
|
||||
if (!pathToCheck) return false
|
||||
let stat
|
||||
return this.representedDirectoryPaths.some(projectPath => {
|
||||
if (pathToCheck === projectPath) return true
|
||||
if (!pathToCheck.startsWith(path.join(projectPath, path.sep))) return false
|
||||
if (stat === undefined) stat = fs.statSyncNoException(pathToCheck)
|
||||
return !stat || !stat.isDirectory()
|
||||
})
|
||||
}
|
||||
|
||||
handleEvents () {
|
||||
this.browserWindow.on('close', async event => {
|
||||
if (!this.atomApplication.quitting && !this.unloading) {
|
||||
event.preventDefault()
|
||||
this.unloading = true
|
||||
this.atomApplication.saveCurrentWindowOptions(false)
|
||||
if (await this.prepareToUnload()) this.close()
|
||||
}
|
||||
})
|
||||
|
||||
this.browserWindow.on('closed', () => {
|
||||
this.fileRecoveryService.didCloseWindow(this)
|
||||
this.atomApplication.removeWindow(this)
|
||||
this.resolveClosedPromise()
|
||||
})
|
||||
|
||||
this.browserWindow.on('unresponsive', () => {
|
||||
if (this.isSpec) return
|
||||
dialog.showMessageBox(this.browserWindow, {
|
||||
type: 'warning',
|
||||
buttons: ['Force Close', 'Keep Waiting'],
|
||||
cancelId: 1, // Canceling should be the least destructive action
|
||||
message: 'Editor is not responding',
|
||||
detail:
|
||||
'The editor is not responding. Would you like to force close it or just keep waiting?'
|
||||
}, response => { if (response === 0) this.browserWindow.destroy() })
|
||||
})
|
||||
|
||||
this.browserWindow.webContents.on('crashed', async () => {
|
||||
if (this.headless) {
|
||||
console.log('Renderer process crashed, exiting')
|
||||
this.atomApplication.exit(100)
|
||||
return
|
||||
}
|
||||
|
||||
await this.fileRecoveryService.didCrashWindow(this)
|
||||
dialog.showMessageBox(this.browserWindow, {
|
||||
type: 'warning',
|
||||
buttons: ['Close Window', 'Reload', 'Keep It Open'],
|
||||
cancelId: 2, // Canceling should be the least destructive action
|
||||
message: 'The editor has crashed',
|
||||
detail: 'Please report this issue to https://github.com/atom/atom'
|
||||
}, response => {
|
||||
switch (response) {
|
||||
case 0: return this.browserWindow.destroy()
|
||||
case 1: return this.browserWindow.reload()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.browserWindow.webContents.on('will-navigate', (event, url) => {
|
||||
if (url !== this.browserWindow.webContents.getURL()) event.preventDefault()
|
||||
})
|
||||
|
||||
this.setupContextMenu()
|
||||
|
||||
// Spec window's web view should always have focus
|
||||
if (this.isSpec) this.browserWindow.on('blur', () => this.browserWindow.focusOnWebView())
|
||||
}
|
||||
|
||||
async prepareToUnload () {
|
||||
if (this.isSpecWindow()) return true
|
||||
|
||||
this.lastPrepareToUnloadPromise = new Promise(resolve => {
|
||||
const callback = (event, result) => {
|
||||
if (BrowserWindow.fromWebContents(event.sender) === this.browserWindow) {
|
||||
ipcMain.removeListener('did-prepare-to-unload', callback)
|
||||
if (!result) {
|
||||
this.unloading = false
|
||||
this.atomApplication.quitting = false
|
||||
}
|
||||
resolve(result)
|
||||
}
|
||||
}
|
||||
ipcMain.on('did-prepare-to-unload', callback)
|
||||
this.browserWindow.webContents.send('prepare-to-unload')
|
||||
})
|
||||
|
||||
return this.lastPrepareToUnloadPromise
|
||||
}
|
||||
|
||||
openPath (pathToOpen, initialLine, initialColumn) {
|
||||
return this.openLocations([{pathToOpen, initialLine, initialColumn}])
|
||||
}
|
||||
|
||||
async openLocations (locationsToOpen) {
|
||||
await this.loadedPromise
|
||||
this.sendMessage('open-locations', locationsToOpen)
|
||||
}
|
||||
|
||||
didChangeUserSettings (settings) {
|
||||
this.sendMessage('did-change-user-settings', settings)
|
||||
}
|
||||
|
||||
didFailToReadUserSettings (message) {
|
||||
this.sendMessage('did-fail-to-read-user-settings', message)
|
||||
}
|
||||
|
||||
replaceEnvironment (env) {
|
||||
this.browserWindow.webContents.send('environment', env)
|
||||
}
|
||||
|
||||
sendMessage (message, detail) {
|
||||
this.browserWindow.webContents.send('message', message, detail)
|
||||
}
|
||||
|
||||
sendCommand (command, ...args) {
|
||||
if (this.isSpecWindow()) {
|
||||
if (!this.atomApplication.sendCommandToFirstResponder(command)) {
|
||||
switch (command) {
|
||||
case 'window:reload': return this.reload()
|
||||
case 'window:toggle-dev-tools': return this.toggleDevTools()
|
||||
case 'window:close': return this.close()
|
||||
}
|
||||
}
|
||||
} else if (this.isWebViewFocused()) {
|
||||
this.sendCommandToBrowserWindow(command, ...args)
|
||||
} else if (!this.atomApplication.sendCommandToFirstResponder(command)) {
|
||||
this.sendCommandToBrowserWindow(command, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
sendURIMessage (uri) {
|
||||
this.browserWindow.webContents.send('uri-message', uri)
|
||||
}
|
||||
|
||||
sendCommandToBrowserWindow (command, ...args) {
|
||||
const action = args[0] && args[0].contextCommand
|
||||
? 'context-command'
|
||||
: 'command'
|
||||
this.browserWindow.webContents.send(action, command, ...args)
|
||||
}
|
||||
|
||||
getDimensions () {
|
||||
const [x, y] = Array.from(this.browserWindow.getPosition())
|
||||
const [width, height] = Array.from(this.browserWindow.getSize())
|
||||
return {x, y, width, height}
|
||||
}
|
||||
|
||||
shouldAddCustomTitleBar () {
|
||||
return (
|
||||
!this.isSpec &&
|
||||
process.platform === 'darwin' &&
|
||||
this.atomApplication.config.get('core.titleBar') === 'custom'
|
||||
)
|
||||
}
|
||||
|
||||
shouldAddCustomInsetTitleBar () {
|
||||
return (
|
||||
!this.isSpec &&
|
||||
process.platform === 'darwin' &&
|
||||
this.atomApplication.config.get('core.titleBar') === 'custom-inset'
|
||||
)
|
||||
}
|
||||
|
||||
shouldHideTitleBar () {
|
||||
return (
|
||||
!this.isSpec &&
|
||||
process.platform === 'darwin' &&
|
||||
this.atomApplication.config.get('core.titleBar') === 'hidden'
|
||||
)
|
||||
}
|
||||
|
||||
close () {
|
||||
return this.browserWindow.close()
|
||||
}
|
||||
|
||||
focus () {
|
||||
return this.browserWindow.focus()
|
||||
}
|
||||
|
||||
minimize () {
|
||||
return this.browserWindow.minimize()
|
||||
}
|
||||
|
||||
maximize () {
|
||||
return this.browserWindow.maximize()
|
||||
}
|
||||
|
||||
unmaximize () {
|
||||
return this.browserWindow.unmaximize()
|
||||
}
|
||||
|
||||
restore () {
|
||||
return this.browserWindow.restore()
|
||||
}
|
||||
|
||||
setFullScreen (fullScreen) {
|
||||
return this.browserWindow.setFullScreen(fullScreen)
|
||||
}
|
||||
|
||||
setAutoHideMenuBar (autoHideMenuBar) {
|
||||
return this.browserWindow.setAutoHideMenuBar(autoHideMenuBar)
|
||||
}
|
||||
|
||||
handlesAtomCommands () {
|
||||
return !this.isSpecWindow() && this.isWebViewFocused()
|
||||
}
|
||||
|
||||
isFocused () {
|
||||
return this.browserWindow.isFocused()
|
||||
}
|
||||
|
||||
isMaximized () {
|
||||
return this.browserWindow.isMaximized()
|
||||
}
|
||||
|
||||
isMinimized () {
|
||||
return this.browserWindow.isMinimized()
|
||||
}
|
||||
|
||||
isWebViewFocused () {
|
||||
return this.browserWindow.isWebViewFocused()
|
||||
}
|
||||
|
||||
isSpecWindow () {
|
||||
return this.isSpec
|
||||
}
|
||||
|
||||
reload () {
|
||||
this.loadedPromise = new Promise(resolve => { this.resolveLoadedPromise = resolve })
|
||||
this.prepareToUnload().then(canUnload => {
|
||||
if (canUnload) this.browserWindow.reload()
|
||||
})
|
||||
return this.loadedPromise
|
||||
}
|
||||
|
||||
showSaveDialog (options, callback) {
|
||||
options = Object.assign({
|
||||
title: 'Save File',
|
||||
defaultPath: this.representedDirectoryPaths[0]
|
||||
}, options)
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
// Async
|
||||
dialog.showSaveDialog(this.browserWindow, options, callback)
|
||||
} else {
|
||||
// Sync
|
||||
return dialog.showSaveDialog(this.browserWindow, options)
|
||||
}
|
||||
}
|
||||
|
||||
toggleDevTools () {
|
||||
return this.browserWindow.toggleDevTools()
|
||||
}
|
||||
|
||||
openDevTools () {
|
||||
return this.browserWindow.openDevTools()
|
||||
}
|
||||
|
||||
closeDevTools () {
|
||||
return this.browserWindow.closeDevTools()
|
||||
}
|
||||
|
||||
setDocumentEdited (documentEdited) {
|
||||
return this.browserWindow.setDocumentEdited(documentEdited)
|
||||
}
|
||||
|
||||
setRepresentedFilename (representedFilename) {
|
||||
return this.browserWindow.setRepresentedFilename(representedFilename)
|
||||
}
|
||||
|
||||
setRepresentedDirectoryPaths (representedDirectoryPaths) {
|
||||
this.representedDirectoryPaths = representedDirectoryPaths
|
||||
this.representedDirectoryPaths.sort()
|
||||
this.loadSettings.initialPaths = this.representedDirectoryPaths
|
||||
return this.atomApplication.saveCurrentWindowOptions()
|
||||
}
|
||||
|
||||
didClosePathWithWaitSession (path) {
|
||||
this.atomApplication.windowDidClosePathWithWaitSession(this, path)
|
||||
}
|
||||
|
||||
copy () {
|
||||
return this.browserWindow.copy()
|
||||
}
|
||||
|
||||
disableZoom () {
|
||||
return this.browserWindow.webContents.setVisualZoomLevelLimits(1, 1)
|
||||
}
|
||||
}
|
||||
178
src/main-process/auto-update-manager.js
Normal file
178
src/main-process/auto-update-manager.js
Normal file
@@ -0,0 +1,178 @@
|
||||
const {EventEmitter} = require('events')
|
||||
const path = require('path')
|
||||
|
||||
const IdleState = 'idle'
|
||||
const CheckingState = 'checking'
|
||||
const DownloadingState = 'downloading'
|
||||
const UpdateAvailableState = 'update-available'
|
||||
const NoUpdateAvailableState = 'no-update-available'
|
||||
const UnsupportedState = 'unsupported'
|
||||
const ErrorState = 'error'
|
||||
|
||||
let autoUpdater = null
|
||||
|
||||
module.exports =
|
||||
class AutoUpdateManager extends EventEmitter {
|
||||
constructor (version, testMode, config) {
|
||||
super()
|
||||
this.onUpdateNotAvailable = this.onUpdateNotAvailable.bind(this)
|
||||
this.onUpdateError = this.onUpdateError.bind(this)
|
||||
this.version = version
|
||||
this.testMode = testMode
|
||||
this.config = config
|
||||
this.state = IdleState
|
||||
this.iconPath = path.resolve(__dirname, '..', '..', 'resources', 'atom.png')
|
||||
}
|
||||
|
||||
initialize () {
|
||||
if (process.platform === 'win32') {
|
||||
const archSuffix = process.arch === 'ia32' ? '' : `-${process.arch}`
|
||||
this.feedUrl = `https://atom.io/api/updates${archSuffix}?version=${this.version}`
|
||||
autoUpdater = require('./auto-updater-win32')
|
||||
} else {
|
||||
this.feedUrl = `https://atom.io/api/updates?version=${this.version}`;
|
||||
({autoUpdater} = require('electron'))
|
||||
}
|
||||
|
||||
autoUpdater.on('error', (event, message) => {
|
||||
this.setState(ErrorState, message)
|
||||
this.emitWindowEvent('update-error')
|
||||
console.error(`Error Downloading Update: ${message}`)
|
||||
})
|
||||
|
||||
autoUpdater.setFeedURL(this.feedUrl)
|
||||
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
this.setState(CheckingState)
|
||||
this.emitWindowEvent('checking-for-update')
|
||||
})
|
||||
|
||||
autoUpdater.on('update-not-available', () => {
|
||||
this.setState(NoUpdateAvailableState)
|
||||
this.emitWindowEvent('update-not-available')
|
||||
})
|
||||
|
||||
autoUpdater.on('update-available', () => {
|
||||
this.setState(DownloadingState)
|
||||
// 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
|
||||
this.emitWindowEvent('did-begin-downloading-update')
|
||||
this.emit('did-begin-download')
|
||||
})
|
||||
|
||||
autoUpdater.on('update-downloaded', (event, releaseNotes, releaseVersion) => {
|
||||
this.releaseVersion = releaseVersion
|
||||
this.setState(UpdateAvailableState)
|
||||
this.emitUpdateAvailableEvent()
|
||||
})
|
||||
|
||||
this.config.onDidChange('core.automaticallyUpdate', ({newValue}) => {
|
||||
if (newValue) {
|
||||
this.scheduleUpdateCheck()
|
||||
} else {
|
||||
this.cancelScheduledUpdateCheck()
|
||||
}
|
||||
})
|
||||
|
||||
if (this.config.get('core.automaticallyUpdate')) this.scheduleUpdateCheck()
|
||||
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
if (!autoUpdater.supportsUpdates()) {
|
||||
this.setState(UnsupportedState)
|
||||
}
|
||||
break
|
||||
case 'linux':
|
||||
this.setState(UnsupportedState)
|
||||
}
|
||||
}
|
||||
|
||||
emitUpdateAvailableEvent () {
|
||||
if (this.releaseVersion == null) return
|
||||
this.emitWindowEvent('update-available', {releaseVersion: this.releaseVersion})
|
||||
}
|
||||
|
||||
emitWindowEvent (eventName, payload) {
|
||||
for (let atomWindow of this.getWindows()) {
|
||||
atomWindow.sendMessage(eventName, payload)
|
||||
}
|
||||
}
|
||||
|
||||
setState (state, errorMessage) {
|
||||
if (this.state === state) return
|
||||
this.state = state
|
||||
this.errorMessage = errorMessage
|
||||
this.emit('state-changed', this.state)
|
||||
}
|
||||
|
||||
getState () {
|
||||
return this.state
|
||||
}
|
||||
|
||||
getErrorMessage () {
|
||||
return this.errorMessage
|
||||
}
|
||||
|
||||
scheduleUpdateCheck () {
|
||||
// Only schedule update check periodically if running in release version and
|
||||
// and there is no existing scheduled update check.
|
||||
if (!/-dev/.test(this.version) && !this.checkForUpdatesIntervalID) {
|
||||
const checkForUpdates = () => this.check({hidePopups: true})
|
||||
const fourHours = 1000 * 60 * 60 * 4
|
||||
this.checkForUpdatesIntervalID = setInterval(checkForUpdates, fourHours)
|
||||
checkForUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
cancelScheduledUpdateCheck () {
|
||||
if (this.checkForUpdatesIntervalID) {
|
||||
clearInterval(this.checkForUpdatesIntervalID)
|
||||
this.checkForUpdatesIntervalID = null
|
||||
}
|
||||
}
|
||||
|
||||
check ({hidePopups} = {}) {
|
||||
if (!hidePopups) {
|
||||
autoUpdater.once('update-not-available', this.onUpdateNotAvailable)
|
||||
autoUpdater.once('error', this.onUpdateError)
|
||||
}
|
||||
|
||||
if (!this.testMode) autoUpdater.checkForUpdates()
|
||||
}
|
||||
|
||||
install () {
|
||||
if (!this.testMode) autoUpdater.quitAndInstall()
|
||||
}
|
||||
|
||||
onUpdateNotAvailable () {
|
||||
autoUpdater.removeListener('error', this.onUpdateError)
|
||||
const {dialog} = require('electron')
|
||||
dialog.showMessageBox({
|
||||
type: 'info',
|
||||
buttons: ['OK'],
|
||||
icon: this.iconPath,
|
||||
message: 'No update available.',
|
||||
title: 'No Update Available',
|
||||
detail: `Version ${this.version} is the latest version.`
|
||||
}, () => {}) // noop callback to get async behavior
|
||||
}
|
||||
|
||||
onUpdateError (event, message) {
|
||||
autoUpdater.removeListener('update-not-available', this.onUpdateNotAvailable)
|
||||
const {dialog} = require('electron')
|
||||
dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
buttons: ['OK'],
|
||||
icon: this.iconPath,
|
||||
message: 'There was an error checking for updates.',
|
||||
title: 'Update Error',
|
||||
detail: message
|
||||
}, () => {}) // noop callback to get async behavior
|
||||
}
|
||||
|
||||
getWindows () {
|
||||
return global.atomApplication.getAllWindows()
|
||||
}
|
||||
}
|
||||
88
src/main-process/auto-updater-win32.js
Normal file
88
src/main-process/auto-updater-win32.js
Normal file
@@ -0,0 +1,88 @@
|
||||
const {EventEmitter} = require('events')
|
||||
const SquirrelUpdate = require('./squirrel-update')
|
||||
|
||||
class AutoUpdater extends EventEmitter {
|
||||
setFeedURL (updateUrl) {
|
||||
this.updateUrl = updateUrl
|
||||
}
|
||||
|
||||
quitAndInstall () {
|
||||
if (SquirrelUpdate.existsSync()) {
|
||||
SquirrelUpdate.restartAtom(require('electron').app)
|
||||
} else {
|
||||
require('electron').autoUpdater.quitAndInstall()
|
||||
}
|
||||
}
|
||||
|
||||
downloadUpdate (callback) {
|
||||
SquirrelUpdate.spawn(['--download', this.updateUrl], function (error, stdout) {
|
||||
let update
|
||||
if (error != null) return callback(error)
|
||||
|
||||
try {
|
||||
// Last line of output is the JSON details about the releases
|
||||
const json = stdout.trim().split('\n').pop()
|
||||
const data = JSON.parse(json)
|
||||
const releasesToApply = data && data.releasesToApply
|
||||
if (releasesToApply.pop) update = releasesToApply.pop()
|
||||
} catch (error) {
|
||||
error.stdout = stdout
|
||||
return callback(error)
|
||||
}
|
||||
|
||||
callback(null, update)
|
||||
})
|
||||
}
|
||||
|
||||
installUpdate (callback) {
|
||||
SquirrelUpdate.spawn(['--update', this.updateUrl], callback)
|
||||
}
|
||||
|
||||
supportsUpdates () {
|
||||
SquirrelUpdate.existsSync()
|
||||
}
|
||||
|
||||
checkForUpdates () {
|
||||
if (!this.updateUrl) throw new Error('Update URL is not set')
|
||||
|
||||
this.emit('checking-for-update')
|
||||
|
||||
if (!SquirrelUpdate.existsSync()) {
|
||||
this.emit('update-not-available')
|
||||
return
|
||||
}
|
||||
|
||||
this.downloadUpdate((error, update) => {
|
||||
if (error != null) {
|
||||
this.emit('update-not-available')
|
||||
return
|
||||
}
|
||||
|
||||
if (update == null) {
|
||||
this.emit('update-not-available')
|
||||
return
|
||||
}
|
||||
|
||||
this.emit('update-available')
|
||||
|
||||
this.installUpdate(error => {
|
||||
if (error != null) {
|
||||
this.emit('update-not-available')
|
||||
return
|
||||
}
|
||||
|
||||
this.emit(
|
||||
'update-downloaded',
|
||||
{},
|
||||
update.releaseNotes,
|
||||
update.version,
|
||||
new Date(),
|
||||
'https://atom.io',
|
||||
() => this.quitAndInstall()
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new AutoUpdater()
|
||||
33
src/main-process/context-menu.js
Normal file
33
src/main-process/context-menu.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const {Menu} = require('electron')
|
||||
|
||||
module.exports =
|
||||
class ContextMenu {
|
||||
constructor (template, atomWindow) {
|
||||
this.atomWindow = atomWindow
|
||||
this.createClickHandlers(template)
|
||||
const menu = Menu.buildFromTemplate(template)
|
||||
menu.popup(this.atomWindow.browserWindow, {async: true})
|
||||
}
|
||||
|
||||
// It's necessary to build the event handlers in this process, otherwise
|
||||
// closures are dragged across processes and failed to be garbage collected
|
||||
// appropriately.
|
||||
createClickHandlers (template) {
|
||||
template.forEach(item => {
|
||||
if (item.command) {
|
||||
if (!item.commandDetail) item.commandDetail = {}
|
||||
item.commandDetail.contextCommand = true
|
||||
item.commandDetail.atomWindow = this.atomWindow
|
||||
item.click = () => {
|
||||
global.atomApplication.sendCommandToWindow(
|
||||
item.command,
|
||||
this.atomWindow,
|
||||
item.commandDetail
|
||||
)
|
||||
}
|
||||
} else if (item.submenu) {
|
||||
this.createClickHandlers(item.submenu)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
165
src/main-process/file-recovery-service.js
Normal file
165
src/main-process/file-recovery-service.js
Normal file
@@ -0,0 +1,165 @@
|
||||
const {dialog} = require('electron')
|
||||
const crypto = require('crypto')
|
||||
const Path = require('path')
|
||||
const fs = require('fs-plus')
|
||||
const mkdirp = require('mkdirp')
|
||||
|
||||
module.exports =
|
||||
class FileRecoveryService {
|
||||
constructor (recoveryDirectory) {
|
||||
this.recoveryDirectory = recoveryDirectory
|
||||
this.recoveryFilesByFilePath = new Map()
|
||||
this.recoveryFilesByWindow = new WeakMap()
|
||||
this.windowsByRecoveryFile = new Map()
|
||||
}
|
||||
|
||||
async willSavePath (window, path) {
|
||||
const stats = await tryStatFile(path)
|
||||
if (!stats) return
|
||||
|
||||
const recoveryPath = Path.join(this.recoveryDirectory, RecoveryFile.fileNameForPath(path))
|
||||
const recoveryFile =
|
||||
this.recoveryFilesByFilePath.get(path) || new RecoveryFile(path, stats.mode, recoveryPath)
|
||||
|
||||
try {
|
||||
await recoveryFile.retain()
|
||||
} catch (err) {
|
||||
console.log(`Couldn't retain ${recoveryFile.recoveryPath}. Code: ${err.code}. Message: ${err.message}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.recoveryFilesByWindow.has(window)) {
|
||||
this.recoveryFilesByWindow.set(window, new Set())
|
||||
}
|
||||
if (!this.windowsByRecoveryFile.has(recoveryFile)) {
|
||||
this.windowsByRecoveryFile.set(recoveryFile, new Set())
|
||||
}
|
||||
|
||||
this.recoveryFilesByWindow.get(window).add(recoveryFile)
|
||||
this.windowsByRecoveryFile.get(recoveryFile).add(window)
|
||||
this.recoveryFilesByFilePath.set(path, recoveryFile)
|
||||
}
|
||||
|
||||
async didSavePath (window, path) {
|
||||
const recoveryFile = this.recoveryFilesByFilePath.get(path)
|
||||
if (recoveryFile != null) {
|
||||
try {
|
||||
await recoveryFile.release()
|
||||
} catch (err) {
|
||||
console.log(`Couldn't release ${recoveryFile.recoveryPath}. Code: ${err.code}. Message: ${err.message}`)
|
||||
}
|
||||
if (recoveryFile.isReleased()) this.recoveryFilesByFilePath.delete(path)
|
||||
this.recoveryFilesByWindow.get(window).delete(recoveryFile)
|
||||
this.windowsByRecoveryFile.get(recoveryFile).delete(window)
|
||||
}
|
||||
}
|
||||
|
||||
async didCrashWindow (window) {
|
||||
if (!this.recoveryFilesByWindow.has(window)) return
|
||||
|
||||
const promises = []
|
||||
for (const recoveryFile of this.recoveryFilesByWindow.get(window)) {
|
||||
promises.push(recoveryFile.recover()
|
||||
.catch(error => {
|
||||
const message = 'A file that Atom was saving could be corrupted'
|
||||
const detail =
|
||||
`Error ${error.code}. There was a crash while saving "${recoveryFile.originalPath}", so this file might be blank or corrupted.\n` +
|
||||
`Atom couldn't recover it automatically, but a recovery file has been saved at: "${recoveryFile.recoveryPath}".`
|
||||
console.log(detail)
|
||||
dialog.showMessageBox(window, {type: 'info', buttons: ['OK'], message, detail}, () => { /* noop callback to get async behavior */ })
|
||||
})
|
||||
.then(() => {
|
||||
for (let window of this.windowsByRecoveryFile.get(recoveryFile)) {
|
||||
this.recoveryFilesByWindow.get(window).delete(recoveryFile)
|
||||
}
|
||||
this.windowsByRecoveryFile.delete(recoveryFile)
|
||||
this.recoveryFilesByFilePath.delete(recoveryFile.originalPath)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
didCloseWindow (window) {
|
||||
if (!this.recoveryFilesByWindow.has(window)) return
|
||||
|
||||
for (let recoveryFile of this.recoveryFilesByWindow.get(window)) {
|
||||
this.windowsByRecoveryFile.get(recoveryFile).delete(window)
|
||||
}
|
||||
this.recoveryFilesByWindow.delete(window)
|
||||
}
|
||||
}
|
||||
|
||||
class RecoveryFile {
|
||||
static fileNameForPath (path) {
|
||||
const extension = Path.extname(path)
|
||||
const basename = Path.basename(path, extension).substring(0, 34)
|
||||
const randomSuffix = crypto.randomBytes(3).toString('hex')
|
||||
return `${basename}-${randomSuffix}${extension}`
|
||||
}
|
||||
|
||||
constructor (originalPath, fileMode, recoveryPath) {
|
||||
this.originalPath = originalPath
|
||||
this.fileMode = fileMode
|
||||
this.recoveryPath = recoveryPath
|
||||
this.refCount = 0
|
||||
}
|
||||
|
||||
async store () {
|
||||
await copyFile(this.originalPath, this.recoveryPath, this.fileMode)
|
||||
}
|
||||
|
||||
async recover () {
|
||||
await copyFile(this.recoveryPath, this.originalPath, this.fileMode)
|
||||
await this.remove()
|
||||
}
|
||||
|
||||
async remove () {
|
||||
return new Promise((resolve, reject) =>
|
||||
fs.unlink(this.recoveryPath, error =>
|
||||
error && error.code !== 'ENOENT' ? reject(error) : resolve()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
async retain () {
|
||||
if (this.isReleased()) await this.store()
|
||||
this.refCount++
|
||||
}
|
||||
|
||||
async release () {
|
||||
this.refCount--
|
||||
if (this.isReleased()) await this.remove()
|
||||
}
|
||||
|
||||
isReleased () {
|
||||
return this.refCount === 0
|
||||
}
|
||||
}
|
||||
|
||||
async function tryStatFile (path) {
|
||||
return new Promise((resolve, reject) =>
|
||||
fs.stat(path, (error, result) =>
|
||||
resolve(error == null && result)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
async function copyFile (source, destination, mode) {
|
||||
return new Promise((resolve, reject) => {
|
||||
mkdirp(Path.dirname(destination), (error) => {
|
||||
if (error) return reject(error)
|
||||
const readStream = fs.createReadStream(source)
|
||||
readStream
|
||||
.on('error', reject)
|
||||
.once('open', () => {
|
||||
const writeStream = fs.createWriteStream(destination, {mode})
|
||||
writeStream
|
||||
.on('error', reject)
|
||||
.on('open', () => readStream.pipe(writeStream))
|
||||
.once('close', () => resolve())
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user