Merge branch 'master' of github.com:atom/atom into pr-11139/atom/ld-change-range-event

This commit is contained in:
Ash Wilson
2018-08-29 15:08:50 -04:00
716 changed files with 158903 additions and 64167 deletions

View File

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

View File

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

File diff suppressed because it is too large Load Diff

60
src/atom-paths.js Normal file
View 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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

1484
src/config.js Normal file

File diff suppressed because it is too large Load Diff

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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'
])

View File

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

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

View File

@@ -1,55 +0,0 @@
module.exports =
class DOMElementPool
constructor: ->
@freeElementsByTagName = {}
@freedElements = new Set
clear: ->
@freedElements.clear()
for tagName, freeElements of @freeElementsByTagName
freeElements.length = 0
return
build: (tagName, factory, reset) ->
element = @freeElementsByTagName[tagName]?.pop()
element ?= factory()
reset(element)
@freedElements.delete(element)
element
buildElement: (tagName, className) ->
factory = -> document.createElement(tagName)
reset = (element) ->
delete element.dataset[dataId] for dataId of element.dataset
element.removeAttribute("style")
if className?
element.className = className
else
element.removeAttribute("class")
@build(tagName, factory, reset)
buildText: (textContent) ->
factory = -> document.createTextNode(textContent)
reset = (element) -> element.textContent = textContent
@build("#text", factory, reset)
freeElementAndDescendants: (element) ->
@free(element)
@freeDescendants(element)
freeDescendants: (element) ->
for descendant in element.childNodes by -1
@free(descendant)
@freeDescendants(descendant)
return
free: (element) ->
throw new Error("The element cannot be null or undefined.") unless element?
throw new Error("The element has already been freed!") if @freedElements.has(element)
tagName = element.nodeName.toLowerCase()
@freeElementsByTagName[tagName] ?= []
@freeElementsByTagName[tagName].push(element)
@freedElements.add(element)
element.remove()

80
src/electron-shims.js Normal file
View 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)
}
}

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

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

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

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

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

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

View 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