mirror of
https://github.com/atom/atom.git
synced 2026-02-19 02:44:29 -05:00
Merge branch 'master' of https://github.com/atom/atom into b3-failing-seed
This commit is contained in:
@@ -1,294 +0,0 @@
|
||||
_ = require 'underscore-plus'
|
||||
{ipcRenderer, remote, shell} = require 'electron'
|
||||
ipcHelpers = require './ipc-helpers'
|
||||
{Disposable} = require 'event-kit'
|
||||
getWindowLoadSettings = require './get-window-load-settings'
|
||||
|
||||
module.exports =
|
||||
class ApplicationDelegate
|
||||
getWindowLoadSettings: -> getWindowLoadSettings()
|
||||
|
||||
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: ->
|
||||
ipcHelpers.call('window-method', 'close')
|
||||
|
||||
getTemporaryWindowState: ->
|
||||
ipcHelpers.call('get-temporary-window-state').then (stateJSON) -> JSON.parse(stateJSON)
|
||||
|
||||
setTemporaryWindowState: (state) ->
|
||||
ipcHelpers.call('set-temporary-window-state', JSON.stringify(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: ->
|
||||
ipcHelpers.call('window-method', 'reload')
|
||||
|
||||
restartApplication: ->
|
||||
ipcRenderer.send("restart-application")
|
||||
|
||||
minimizeWindow: ->
|
||||
ipcHelpers.call('window-method', 'minimize')
|
||||
|
||||
isWindowMaximized: ->
|
||||
remote.getCurrentWindow().isMaximized()
|
||||
|
||||
maximizeWindow: ->
|
||||
ipcHelpers.call('window-method', 'maximize')
|
||||
|
||||
unmaximizeWindow: ->
|
||||
ipcHelpers.call('window-method', 'unmaximize')
|
||||
|
||||
isWindowFullScreen: ->
|
||||
remote.getCurrentWindow().isFullScreen()
|
||||
|
||||
setWindowFullScreen: (fullScreen=false) ->
|
||||
ipcHelpers.call('window-method', 'setFullScreen', fullScreen)
|
||||
|
||||
onDidEnterFullScreen: (callback) ->
|
||||
ipcHelpers.on(ipcRenderer, 'did-enter-full-screen', callback)
|
||||
|
||||
onDidLeaveFullScreen: (callback) ->
|
||||
ipcHelpers.on(ipcRenderer, 'did-leave-full-screen', callback)
|
||||
|
||||
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).
|
||||
new Promise(process.nextTick).then(-> ipcHelpers.call('window-method', 'openDevTools'))
|
||||
|
||||
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).
|
||||
new Promise(process.nextTick).then(-> ipcHelpers.call('window-method', 'closeDevTools'))
|
||||
|
||||
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).
|
||||
new Promise(process.nextTick).then(-> ipcHelpers.call('window-method', 'toggleDevTools'))
|
||||
|
||||
executeJavaScriptInWindowDevTools: (code) ->
|
||||
ipcRenderer.send("execute-javascript-in-dev-tools", code)
|
||||
|
||||
setWindowDocumentEdited: (edited) ->
|
||||
ipcHelpers.call('window-method', 'setDocumentEdited', edited)
|
||||
|
||||
setRepresentedFilename: (filename) ->
|
||||
ipcHelpers.call('window-method', 'setRepresentedFilename', filename)
|
||||
|
||||
addRecentDocument: (filename) ->
|
||||
ipcRenderer.send("add-recent-document", filename)
|
||||
|
||||
setRepresentedDirectoryPaths: (paths) ->
|
||||
ipcHelpers.call('window-method', 'setRepresentedDirectoryPaths', paths)
|
||||
|
||||
setAutoHideWindowMenuBar: (autoHide) ->
|
||||
ipcHelpers.call('window-method', 'setAutoHideMenuBar', autoHide)
|
||||
|
||||
setWindowMenuBarVisibility: (visible) ->
|
||||
remote.getCurrentWindow().setMenuBarVisibility(visible)
|
||||
|
||||
getPrimaryDisplayWorkAreaSize: ->
|
||||
remote.screen.getPrimaryDisplay().workAreaSize
|
||||
|
||||
getUserDefault: (key, type) ->
|
||||
remote.systemPreferences.getUserDefault(key, type)
|
||||
|
||||
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
|
||||
normalizeAccessKeys: true
|
||||
})
|
||||
|
||||
if _.isArray(buttons)
|
||||
chosen
|
||||
else
|
||||
callback = buttons[buttonLabels[chosen]]
|
||||
callback?()
|
||||
|
||||
showMessageDialog: (params) ->
|
||||
|
||||
showSaveDialog: (params) ->
|
||||
if typeof params is 'string'
|
||||
params = {defaultPath: params}
|
||||
@getCurrentWindow().showSaveDialog(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)
|
||||
|
||||
onUpdateError: (callback) ->
|
||||
outerCallback = (event, message, detail) ->
|
||||
callback(detail) if message is 'update-error'
|
||||
|
||||
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)
|
||||
|
||||
onURIMessage: (callback) ->
|
||||
outerCallback = (event, args...) ->
|
||||
callback(args...)
|
||||
|
||||
ipcRenderer.on('uri-message', outerCallback)
|
||||
new Disposable ->
|
||||
ipcRenderer.removeListener('uri-message', outerCallback)
|
||||
|
||||
onDidRequestUnload: (callback) ->
|
||||
outerCallback = (event, message) ->
|
||||
callback(event).then (shouldUnload) ->
|
||||
ipcRenderer.send('did-prepare-to-unload', shouldUnload)
|
||||
|
||||
ipcRenderer.on('prepare-to-unload', outerCallback)
|
||||
new Disposable ->
|
||||
ipcRenderer.removeListener('prepare-to-unload', outerCallback)
|
||||
|
||||
onDidChangeHistoryManager: (callback) ->
|
||||
outerCallback = (event, message) ->
|
||||
callback(event)
|
||||
|
||||
ipcRenderer.on('did-change-history-manager', outerCallback)
|
||||
new Disposable ->
|
||||
ipcRenderer.removeListener('did-change-history-manager', outerCallback)
|
||||
|
||||
didChangeHistoryManager: ->
|
||||
ipcRenderer.send('did-change-history-manager')
|
||||
|
||||
openExternal: (url) ->
|
||||
shell.openExternal(url)
|
||||
|
||||
checkForUpdate: ->
|
||||
ipcRenderer.send('command', 'application:check-for-update')
|
||||
|
||||
restartAndInstallUpdate: ->
|
||||
ipcRenderer.send('command', 'application:install-update')
|
||||
|
||||
getAutoUpdateManagerState: ->
|
||||
ipcRenderer.sendSync('get-auto-update-manager-state')
|
||||
|
||||
getAutoUpdateManagerErrorMessage: ->
|
||||
ipcRenderer.sendSync('get-auto-update-manager-error')
|
||||
|
||||
emitWillSavePath: (path) ->
|
||||
ipcRenderer.sendSync('will-save-path', path)
|
||||
|
||||
emitDidSavePath: (path) ->
|
||||
ipcRenderer.sendSync('did-save-path', path)
|
||||
|
||||
resolveProxy: (requestId, url) ->
|
||||
ipcRenderer.send('resolve-proxy', requestId, url)
|
||||
|
||||
onDidResolveProxy: (callback) ->
|
||||
outerCallback = (event, requestId, proxy) ->
|
||||
callback(requestId, proxy)
|
||||
|
||||
ipcRenderer.on('did-resolve-proxy', outerCallback)
|
||||
new Disposable ->
|
||||
ipcRenderer.removeListener('did-resolve-proxy', outerCallback)
|
||||
374
src/application-delegate.js
Normal file
374
src/application-delegate.js
Normal file
@@ -0,0 +1,374 @@
|
||||
const {ipcRenderer, remote, shell} = require('electron')
|
||||
const ipcHelpers = require('./ipc-helpers')
|
||||
const {Disposable} = require('event-kit')
|
||||
const getWindowLoadSettings = require('./get-window-load-settings')
|
||||
|
||||
module.exports =
|
||||
class ApplicationDelegate {
|
||||
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)
|
||||
}
|
||||
|
||||
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') callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showMessageDialog (params) {}
|
||||
|
||||
showSaveDialog (options, callback) {
|
||||
if (typeof callback === 'function') {
|
||||
// Async
|
||||
this.getCurrentWindow().showSaveDialog(options, callback)
|
||||
} else {
|
||||
// Sync
|
||||
if (typeof params === 'string') {
|
||||
options = {defaultPath: options}
|
||||
}
|
||||
return this.getCurrentWindow().showSaveDialog(options)
|
||||
}
|
||||
}
|
||||
|
||||
playBeepSound () {
|
||||
return shell.beep()
|
||||
}
|
||||
|
||||
onDidOpenLocations (callback) {
|
||||
const outerCallback = (event, message, detail) => {
|
||||
if (message === 'open-locations') callback(detail)
|
||||
}
|
||||
|
||||
ipcRenderer.on('message', outerCallback)
|
||||
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
|
||||
}
|
||||
|
||||
onUpdateAvailable (callback) {
|
||||
const 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.
|
||||
if (message === 'did-begin-downloading-update') callback(detail)
|
||||
}
|
||||
|
||||
ipcRenderer.on('message', outerCallback)
|
||||
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
|
||||
}
|
||||
|
||||
onDidBeginDownloadingUpdate (callback) {
|
||||
return this.onUpdateAvailable(callback)
|
||||
}
|
||||
|
||||
onDidBeginCheckingForUpdate (callback) {
|
||||
const outerCallback = (event, message, detail) => {
|
||||
if (message === 'checking-for-update') callback(detail)
|
||||
}
|
||||
|
||||
ipcRenderer.on('message', outerCallback)
|
||||
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
|
||||
}
|
||||
|
||||
onDidCompleteDownloadingUpdate (callback) {
|
||||
const outerCallback = (event, message, detail) => {
|
||||
// TODO: We could rename this event to `did-complete-downloading-update`
|
||||
if (message === 'update-available') callback(detail)
|
||||
}
|
||||
|
||||
ipcRenderer.on('message', outerCallback)
|
||||
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
|
||||
}
|
||||
|
||||
onUpdateNotAvailable (callback) {
|
||||
const outerCallback = (event, message, detail) => {
|
||||
if (message === 'update-not-available') callback(detail)
|
||||
}
|
||||
|
||||
ipcRenderer.on('message', outerCallback)
|
||||
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
|
||||
}
|
||||
|
||||
onUpdateError (callback) {
|
||||
const outerCallback = (event, message, detail) => {
|
||||
if (message === 'update-error') callback(detail)
|
||||
}
|
||||
|
||||
ipcRenderer.on('message', outerCallback)
|
||||
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
|
||||
}
|
||||
|
||||
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 ipcRenderer.sendSync('will-save-path', path)
|
||||
}
|
||||
|
||||
emitDidSavePath (path) {
|
||||
return ipcRenderer.sendSync('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))
|
||||
}
|
||||
}
|
||||
@@ -51,13 +51,15 @@ let nextId = 0
|
||||
//
|
||||
// An instance of this class is always available as the `atom` global.
|
||||
class AtomEnvironment {
|
||||
|
||||
/*
|
||||
Section: Construction and Destruction
|
||||
Section: Properties
|
||||
*/
|
||||
|
||||
// Call .loadOrCreate instead
|
||||
constructor (params = {}) {
|
||||
this.id = (params.id != null) ? params.id : nextId++
|
||||
|
||||
// Public: A {Clipboard} instance
|
||||
this.clipboard = params.clipboard
|
||||
this.updateProcessEnv = params.updateProcessEnv || updateProcessEnv
|
||||
this.enablePersistence = params.enablePersistence
|
||||
@@ -68,26 +70,44 @@ class AtomEnvironment {
|
||||
this.loadTime = null
|
||||
this.emitter = new Emitter()
|
||||
this.disposables = new CompositeDisposable()
|
||||
this.pathsWithWaitSessions = new Set()
|
||||
|
||||
// Public: A {DeserializerManager} instance
|
||||
this.deserializers = new DeserializerManager(this)
|
||||
this.deserializeTimings = {}
|
||||
|
||||
// Public: A {ViewRegistry} instance
|
||||
this.views = new ViewRegistry(this)
|
||||
TextEditor.setScheduler(this.views)
|
||||
|
||||
// Public: A {NotificationManager} instance
|
||||
this.notifications = new NotificationManager()
|
||||
|
||||
this.stateStore = new StateStore('AtomEnvironments', 1)
|
||||
|
||||
// Public: A {Config} instance
|
||||
this.config = new Config({
|
||||
notificationManager: this.notifications,
|
||||
enablePersistence: this.enablePersistence
|
||||
})
|
||||
this.config.setSchema(null, {type: 'object', properties: _.clone(ConfigSchema)})
|
||||
|
||||
// Public: A {KeymapManager} instance
|
||||
this.keymaps = new KeymapManager({notificationManager: this.notifications})
|
||||
|
||||
// Public: A {TooltipManager} instance
|
||||
this.tooltips = new TooltipManager({keymapManager: this.keymaps, viewRegistry: this.views})
|
||||
|
||||
// Public: A {CommandRegistry} instance
|
||||
this.commands = new CommandRegistry()
|
||||
this.uriHandlerRegistry = new URIHandlerRegistry()
|
||||
|
||||
// Public: A {GrammarRegistry} instance
|
||||
this.grammars = new GrammarRegistry({config: this.config})
|
||||
|
||||
// Public: A {StyleManager} instance
|
||||
this.styles = new StyleManager()
|
||||
|
||||
// Public: A {PackageManager} instance
|
||||
this.packages = new PackageManager({
|
||||
config: this.config,
|
||||
styleManager: this.styles,
|
||||
@@ -99,6 +119,8 @@ class AtomEnvironment {
|
||||
viewRegistry: this.views,
|
||||
uriHandlerRegistry: this.uriHandlerRegistry
|
||||
})
|
||||
|
||||
// Public: A {ThemeManager} instance
|
||||
this.themes = new ThemeManager({
|
||||
packageManager: this.packages,
|
||||
config: this.config,
|
||||
@@ -106,16 +128,29 @@ class AtomEnvironment {
|
||||
notificationManager: this.notifications,
|
||||
viewRegistry: this.views
|
||||
})
|
||||
|
||||
// Public: A {MenuManager} instance
|
||||
this.menu = new MenuManager({keymapManager: this.keymaps, packageManager: this.packages})
|
||||
|
||||
// Public: A {ContextMenuManager} instance
|
||||
this.contextMenu = new ContextMenuManager({keymapManager: this.keymaps})
|
||||
|
||||
this.packages.setMenuManager(this.menu)
|
||||
this.packages.setContextMenuManager(this.contextMenu)
|
||||
this.packages.setThemeManager(this.themes)
|
||||
|
||||
this.project = new Project({notificationManager: this.notifications, packageManager: this.packages, config: this.config, applicationDelegate: this.applicationDelegate})
|
||||
// Public: A {Project} instance
|
||||
this.project = new Project({
|
||||
notificationManager: this.notifications,
|
||||
packageManager: this.packages,
|
||||
grammarRegistry: this.grammars,
|
||||
config: this.config,
|
||||
applicationDelegate: this.applicationDelegate
|
||||
})
|
||||
this.commandInstaller = new CommandInstaller(this.applicationDelegate)
|
||||
this.protocolHandlerInstaller = new ProtocolHandlerInstaller()
|
||||
|
||||
// Public: A {TextEditorRegistry} instance
|
||||
this.textEditors = new TextEditorRegistry({
|
||||
config: this.config,
|
||||
grammarRegistry: this.grammars,
|
||||
@@ -123,6 +158,7 @@ class AtomEnvironment {
|
||||
packageManager: this.packages
|
||||
})
|
||||
|
||||
// Public: A {Workspace} instance
|
||||
this.workspace = new Workspace({
|
||||
config: this.config,
|
||||
project: this.project,
|
||||
@@ -152,7 +188,9 @@ class AtomEnvironment {
|
||||
|
||||
this.windowEventHandler = new WindowEventHandler({atomEnvironment: this, applicationDelegate: this.applicationDelegate})
|
||||
|
||||
// Public: A {HistoryManager} instance
|
||||
this.history = new HistoryManager({project: this.project, commands: this.commands, stateStore: this.stateStore})
|
||||
|
||||
// Keep instances of HistoryManager in sync
|
||||
this.disposables.add(this.history.onDidChangeProjects(event => {
|
||||
if (!event.reloaded) this.applicationDelegate.didChangeHistoryManager()
|
||||
@@ -201,12 +239,13 @@ class AtomEnvironment {
|
||||
this.themes.initialize({configDirPath: this.configDirPath, resourcePath, safeMode, devMode})
|
||||
|
||||
this.commandInstaller.initialize(this.getVersion())
|
||||
this.protocolHandlerInstaller.initialize(this.config, this.notifications)
|
||||
this.uriHandlerRegistry.registerHostHandler('core', CoreURIHandlers.create(this))
|
||||
this.autoUpdater.initialize()
|
||||
|
||||
this.config.load()
|
||||
|
||||
this.protocolHandlerInstaller.initialize(this.config, this.notifications)
|
||||
|
||||
this.themes.loadBaseStylesheets()
|
||||
this.initialStyleElements = this.styles.getSnapshot()
|
||||
if (params.onlyLoadBaseStyleSheets) this.themes.initialLoadComplete = true
|
||||
@@ -321,6 +360,7 @@ class AtomEnvironment {
|
||||
this.grammars.clear()
|
||||
this.textEditors.clear()
|
||||
this.views.clear()
|
||||
this.pathsWithWaitSessions.clear()
|
||||
}
|
||||
|
||||
destroy () {
|
||||
@@ -333,7 +373,7 @@ class AtomEnvironment {
|
||||
if (this.project) this.project.destroy()
|
||||
this.project = null
|
||||
this.commands.clear()
|
||||
this.stylesElement.remove()
|
||||
if (this.stylesElement) this.stylesElement.remove()
|
||||
this.config.unobserveUserConfig()
|
||||
this.autoUpdater.destroy()
|
||||
this.uriHandlerRegistry.destroy()
|
||||
@@ -784,7 +824,22 @@ class AtomEnvironment {
|
||||
this.document.body.appendChild(this.workspace.getElement())
|
||||
if (this.backgroundStylesheet) this.backgroundStylesheet.remove()
|
||||
|
||||
this.watchProjectPaths()
|
||||
let previousProjectPaths = this.project.getPaths()
|
||||
this.disposables.add(this.project.onDidChangePaths(newPaths => {
|
||||
for (let path of previousProjectPaths) {
|
||||
if (this.pathsWithWaitSessions.has(path) && !newPaths.includes(path)) {
|
||||
this.applicationDelegate.didClosePathWithWaitSession(path)
|
||||
}
|
||||
}
|
||||
previousProjectPaths = newPaths
|
||||
this.applicationDelegate.setRepresentedDirectoryPaths(newPaths)
|
||||
}))
|
||||
this.disposables.add(this.workspace.onDidDestroyPaneItem(({item}) => {
|
||||
const path = item.getPath && item.getPath()
|
||||
if (this.pathsWithWaitSessions.has(path)) {
|
||||
this.applicationDelegate.didClosePathWithWaitSession(path)
|
||||
}
|
||||
}))
|
||||
|
||||
this.packages.activate()
|
||||
this.keymaps.loadUserKeymap()
|
||||
@@ -815,10 +870,9 @@ class AtomEnvironment {
|
||||
project: this.project.serialize(options),
|
||||
workspace: this.workspace.serialize(),
|
||||
packageStates: this.packages.serialize(),
|
||||
grammars: {grammarOverridesByPath: this.grammars.grammarOverridesByPath},
|
||||
grammars: this.grammars.serialize(),
|
||||
fullScreen: this.isFullScreen(),
|
||||
windowDimensions: this.windowDimensions,
|
||||
textEditors: this.textEditors.serialize()
|
||||
windowDimensions: this.windowDimensions
|
||||
}
|
||||
}
|
||||
|
||||
@@ -911,29 +965,63 @@ class AtomEnvironment {
|
||||
|
||||
// Essential: A flexible way to open a dialog akin to an alert dialog.
|
||||
//
|
||||
// While both async and sync versions are provided, it is recommended to use the async version
|
||||
// such that the renderer process is not blocked while the dialog box is open.
|
||||
//
|
||||
// The async version accepts the same options as Electron's `dialog.showMessageBox`.
|
||||
// For convenience, it sets `type` to `'info'` and `normalizeAccessKeys` to `true` by default.
|
||||
//
|
||||
// If the dialog is closed (via `Esc` key or `X` in the top corner) without selecting a button
|
||||
// the first button will be clicked unless a "Cancel" or "No" button is provided.
|
||||
//
|
||||
// ## Examples
|
||||
//
|
||||
// ```coffee
|
||||
// atom.confirm
|
||||
// message: 'How you feeling?'
|
||||
// detailedMessage: 'Be honest.'
|
||||
// buttons:
|
||||
// Good: -> window.alert('good to hear')
|
||||
// Bad: -> window.alert('bummer')
|
||||
// ```js
|
||||
// // Async version (recommended)
|
||||
// atom.confirm({
|
||||
// message: 'How you feeling?',
|
||||
// detail: 'Be honest.',
|
||||
// buttons: ['Good', 'Bad']
|
||||
// }, response => {
|
||||
// if (response === 0) {
|
||||
// window.alert('good to hear')
|
||||
// } else {
|
||||
// window.alert('bummer')
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// ```js
|
||||
// // Legacy sync version
|
||||
// const chosen = 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:
|
||||
// * `options` An options {Object}. If the callback argument is also supplied, see the documentation at
|
||||
// https://electronjs.org/docs/api/dialog#dialogshowmessageboxbrowserwindow-options-callback for the list of
|
||||
// available options. Otherwise, only the following keys are accepted:
|
||||
// * `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.
|
||||
// * `buttons` (optional) Either an {Array} of {String}s or an {Object} where keys are
|
||||
// button names and the values are callback {Function}s to invoke when clicked.
|
||||
// * `callback` (optional) A {Function} that will be called with the index of the chosen option.
|
||||
// If a callback is supplied, the dialog will be non-blocking. This argument is recommended.
|
||||
//
|
||||
// Returns the chosen button index {Number} if the buttons option is an array or the return value of the callback if the buttons option is an object.
|
||||
confirm (params = {}) {
|
||||
return this.applicationDelegate.confirm(params)
|
||||
// Returns the chosen button index {Number} if the buttons option is an array
|
||||
// or the return value of the callback if the buttons option is an object.
|
||||
// If a callback function is supplied, returns `undefined`.
|
||||
confirm (options = {}, callback) {
|
||||
if (callback) {
|
||||
// Async: no return value
|
||||
this.applicationDelegate.confirm(options, callback)
|
||||
} else {
|
||||
return this.applicationDelegate.confirm(options)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -988,13 +1076,6 @@ class AtomEnvironment {
|
||||
return this.themes.load()
|
||||
}
|
||||
|
||||
// Notify the browser project of the window's current project path
|
||||
watchProjectPaths () {
|
||||
this.disposables.add(this.project.onDidChangePaths(() => {
|
||||
this.applicationDelegate.setRepresentedDirectoryPaths(this.project.getPaths())
|
||||
}))
|
||||
}
|
||||
|
||||
setDocumentEdited (edited) {
|
||||
if (typeof this.applicationDelegate.setWindowDocumentEdited === 'function') {
|
||||
this.applicationDelegate.setWindowDocumentEdited(edited)
|
||||
@@ -1008,8 +1089,10 @@ class AtomEnvironment {
|
||||
}
|
||||
|
||||
addProjectFolder () {
|
||||
this.pickFolder((selectedPaths = []) => {
|
||||
this.addToProject(selectedPaths)
|
||||
return new Promise((resolve) => {
|
||||
this.pickFolder((selectedPaths) => {
|
||||
this.addToProject(selectedPaths || []).then(resolve)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1022,7 +1105,7 @@ class AtomEnvironment {
|
||||
}
|
||||
}
|
||||
|
||||
attemptRestoreProjectStateForPaths (state, projectPaths, filesToOpen = []) {
|
||||
async attemptRestoreProjectStateForPaths (state, projectPaths, filesToOpen = []) {
|
||||
const center = this.workspace.getCenter()
|
||||
const windowIsUnused = () => {
|
||||
for (let container of this.workspace.getPaneContainers()) {
|
||||
@@ -1038,33 +1121,41 @@ class AtomEnvironment {
|
||||
}
|
||||
|
||||
if (windowIsUnused()) {
|
||||
this.restoreStateIntoThisEnvironment(state)
|
||||
await this.restoreStateIntoThisEnvironment(state)
|
||||
return Promise.all(filesToOpen.map(file => this.workspace.open(file)))
|
||||
} else {
|
||||
let resolveDiscardStatePromise = null
|
||||
const discardStatePromise = new Promise((resolve) => {
|
||||
resolveDiscardStatePromise = resolve
|
||||
})
|
||||
const nouns = projectPaths.length === 1 ? 'folder' : 'folders'
|
||||
const choice = this.confirm({
|
||||
this.confirm({
|
||||
message: 'Previous automatically-saved project state detected',
|
||||
detailedMessage: `There is previously saved state for the selected ${nouns}. ` +
|
||||
detail: `There is previously saved state for the selected ${nouns}. ` +
|
||||
`Would you like to add the ${nouns} to this window, permanently discarding the saved state, ` +
|
||||
`or open the ${nouns} in a new window, restoring the saved state?`,
|
||||
buttons: [
|
||||
'&Open in new window and recover state',
|
||||
'&Add to this window and discard state'
|
||||
]})
|
||||
if (choice === 0) {
|
||||
this.open({
|
||||
pathsToOpen: projectPaths.concat(filesToOpen),
|
||||
newWindow: true,
|
||||
devMode: this.inDevMode(),
|
||||
safeMode: this.inSafeMode()
|
||||
})
|
||||
return Promise.resolve(null)
|
||||
} else if (choice === 1) {
|
||||
for (let selectedPath of projectPaths) {
|
||||
this.project.addPath(selectedPath)
|
||||
]
|
||||
}, response => {
|
||||
if (response === 0) {
|
||||
this.open({
|
||||
pathsToOpen: projectPaths.concat(filesToOpen),
|
||||
newWindow: true,
|
||||
devMode: this.inDevMode(),
|
||||
safeMode: this.inSafeMode()
|
||||
})
|
||||
resolveDiscardStatePromise(Promise.resolve(null))
|
||||
} else if (response === 1) {
|
||||
for (let selectedPath of projectPaths) {
|
||||
this.project.addPath(selectedPath)
|
||||
}
|
||||
resolveDiscardStatePromise(Promise.all(filesToOpen.map(file => this.workspace.open(file))))
|
||||
}
|
||||
return Promise.all(filesToOpen.map(file => this.workspace.open(file)))
|
||||
}
|
||||
})
|
||||
|
||||
return discardStatePromise
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1076,12 +1167,11 @@ class AtomEnvironment {
|
||||
return this.deserialize(state)
|
||||
}
|
||||
|
||||
showSaveDialog (callback) {
|
||||
callback(this.showSaveDialogSync())
|
||||
}
|
||||
|
||||
showSaveDialogSync (options = {}) {
|
||||
this.applicationDelegate.showSaveDialog(options)
|
||||
deprecate(`atom.showSaveDialogSync is deprecated and will be removed soon.
|
||||
Please, implement ::saveAs and ::getSaveDialogOptions instead for pane items
|
||||
or use Pane::saveItemAs for programmatic saving.`)
|
||||
return this.applicationDelegate.showSaveDialog(options)
|
||||
}
|
||||
|
||||
async saveState (options, storageKey) {
|
||||
@@ -1112,11 +1202,6 @@ class AtomEnvironment {
|
||||
async deserialize (state) {
|
||||
if (!state) return Promise.resolve()
|
||||
|
||||
const grammarOverridesByPath = state.grammars && state.grammars.grammarOverridesByPath
|
||||
if (grammarOverridesByPath) {
|
||||
this.grammars.grammarOverridesByPath = grammarOverridesByPath
|
||||
}
|
||||
|
||||
this.setFullScreen(state.fullScreen)
|
||||
|
||||
const missingProjectPaths = []
|
||||
@@ -1141,7 +1226,7 @@ class AtomEnvironment {
|
||||
|
||||
this.deserializeTimings.project = Date.now() - startTime
|
||||
|
||||
if (state.textEditors) this.textEditors.deserialize(state.textEditors)
|
||||
if (state.grammars) this.grammars.deserialize(state.grammars)
|
||||
|
||||
startTime = Date.now()
|
||||
if (state.workspace) this.workspace.deserialize(state.workspace, this.deserializers)
|
||||
@@ -1266,8 +1351,9 @@ class AtomEnvironment {
|
||||
}
|
||||
}
|
||||
|
||||
for (var {pathToOpen, initialLine, initialColumn, forceAddToWindow} of locations) {
|
||||
if (pathToOpen && (needsProjectPaths || forceAddToWindow)) {
|
||||
for (const location of locations) {
|
||||
const {pathToOpen} = location
|
||||
if (pathToOpen && (needsProjectPaths || location.forceAddToWindow)) {
|
||||
if (fs.existsSync(pathToOpen)) {
|
||||
pushFolderToOpen(this.project.getDirectoryForProjectPath(pathToOpen).getPath())
|
||||
} else if (fs.existsSync(path.dirname(pathToOpen))) {
|
||||
@@ -1278,8 +1364,10 @@ class AtomEnvironment {
|
||||
}
|
||||
|
||||
if (!fs.isDirectorySync(pathToOpen)) {
|
||||
fileLocationsToOpen.push({pathToOpen, initialLine, initialColumn})
|
||||
fileLocationsToOpen.push(location)
|
||||
}
|
||||
|
||||
if (location.hasWaitSession) this.pathsWithWaitSessions.add(pathToOpen)
|
||||
}
|
||||
|
||||
let restoredState = false
|
||||
@@ -1300,7 +1388,7 @@ class AtomEnvironment {
|
||||
|
||||
if (!restoredState) {
|
||||
const fileOpenPromises = []
|
||||
for ({pathToOpen, initialLine, initialColumn} of fileLocationsToOpen) {
|
||||
for (const {pathToOpen, initialLine, initialColumn} of fileLocationsToOpen) {
|
||||
fileOpenPromises.push(this.workspace && this.workspace.open(pathToOpen, {initialLine, initialColumn}))
|
||||
}
|
||||
await Promise.all(fileOpenPromises)
|
||||
|
||||
@@ -89,6 +89,10 @@ export default class Color {
|
||||
return this.alpha === 1 ? this.toHexString() : this.toRGBAString()
|
||||
}
|
||||
|
||||
toString () {
|
||||
return this.toRGBAString()
|
||||
}
|
||||
|
||||
isEqual (color) {
|
||||
if (this === color) {
|
||||
return true
|
||||
|
||||
@@ -23,8 +23,8 @@ class CommandInstaller {
|
||||
const showErrorDialog = (error) => {
|
||||
this.applicationDelegate.confirm({
|
||||
message: 'Failed to install shell commands',
|
||||
detailedMessage: error.message
|
||||
})
|
||||
detail: error.message
|
||||
}, () => {})
|
||||
}
|
||||
|
||||
this.installAtomCommand(true, error => {
|
||||
@@ -33,8 +33,8 @@ class CommandInstaller {
|
||||
if (error) return showErrorDialog(error)
|
||||
this.applicationDelegate.confirm({
|
||||
message: 'Commands installed.',
|
||||
detailedMessage: 'The shell commands `atom` and `apm` are installed.'
|
||||
})
|
||||
detail: 'The shell commands `atom` and `apm` are installed.'
|
||||
}, () => {})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -309,7 +309,7 @@ module.exports = class CommandRegistry {
|
||||
handleCommandEvent (event) {
|
||||
let propagationStopped = false
|
||||
let immediatePropagationStopped = false
|
||||
let matched = false
|
||||
let matched = []
|
||||
let currentTarget = event.target
|
||||
|
||||
const dispatchedEvent = new CustomEvent(event.type, {
|
||||
@@ -373,10 +373,6 @@ module.exports = class CommandRegistry {
|
||||
listeners = selectorBasedListeners.concat(listeners)
|
||||
}
|
||||
|
||||
if (listeners.length > 0) {
|
||||
matched = true
|
||||
}
|
||||
|
||||
// Call inline listeners first in reverse registration order,
|
||||
// and selector-based listeners by specificity and reverse
|
||||
// registration order.
|
||||
@@ -385,7 +381,7 @@ module.exports = class CommandRegistry {
|
||||
if (immediatePropagationStopped) {
|
||||
break
|
||||
}
|
||||
listener.didDispatch.call(currentTarget, dispatchedEvent)
|
||||
matched.push(listener.didDispatch.call(currentTarget, dispatchedEvent))
|
||||
}
|
||||
|
||||
if (currentTarget === window) {
|
||||
@@ -399,7 +395,7 @@ module.exports = class CommandRegistry {
|
||||
|
||||
this.emitter.emit('did-dispatch', dispatchedEvent)
|
||||
|
||||
return matched
|
||||
return (matched.length > 0 ? Promise.all(matched) : null)
|
||||
}
|
||||
|
||||
commandRegistered (commandName) {
|
||||
|
||||
@@ -342,6 +342,11 @@ const configSchema = {
|
||||
description: 'Emulated with Atom events'
|
||||
}
|
||||
]
|
||||
},
|
||||
useTreeSitterParsers: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Use the new Tree-sitter parsing system for supported languages'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -423,6 +423,7 @@ class Config
|
||||
@configFileHasErrors = false
|
||||
@transactDepth = 0
|
||||
@pendingOperations = []
|
||||
@legacyScopeAliases = {}
|
||||
|
||||
@requestLoad = _.debounce =>
|
||||
@loadUserConfig()
|
||||
@@ -599,11 +600,22 @@ class Config
|
||||
# * `value` The value for the key-path
|
||||
getAll: (keyPath, options) ->
|
||||
{scope} = options if options?
|
||||
result = []
|
||||
|
||||
if scope?
|
||||
scopeDescriptor = ScopeDescriptor.fromObject(scope)
|
||||
result = result.concat @scopedSettingsStore.getAll(scopeDescriptor.getScopeChain(), keyPath, options)
|
||||
result = @scopedSettingsStore.getAll(
|
||||
scopeDescriptor.getScopeChain(),
|
||||
keyPath,
|
||||
options
|
||||
)
|
||||
if legacyScopeDescriptor = @getLegacyScopeDescriptor(scopeDescriptor)
|
||||
result.push(@scopedSettingsStore.getAll(
|
||||
legacyScopeDescriptor.getScopeChain(),
|
||||
keyPath,
|
||||
options
|
||||
)...)
|
||||
else
|
||||
result = []
|
||||
|
||||
if globalValue = @getRawValue(keyPath, options)
|
||||
result.push(scopeSelector: '*', value: globalValue)
|
||||
@@ -762,6 +774,12 @@ class Config
|
||||
finally
|
||||
@endTransaction()
|
||||
|
||||
addLegacyScopeAlias: (languageId, legacyScopeName) ->
|
||||
@legacyScopeAliases[languageId] = legacyScopeName
|
||||
|
||||
removeLegacyScopeAlias: (languageId) ->
|
||||
delete @legacyScopeAliases[languageId]
|
||||
|
||||
###
|
||||
Section: Internal methods used by core
|
||||
###
|
||||
@@ -1145,7 +1163,20 @@ class Config
|
||||
|
||||
getRawScopedValue: (scopeDescriptor, keyPath, options) ->
|
||||
scopeDescriptor = ScopeDescriptor.fromObject(scopeDescriptor)
|
||||
@scopedSettingsStore.getPropertyValue(scopeDescriptor.getScopeChain(), keyPath, options)
|
||||
result = @scopedSettingsStore.getPropertyValue(
|
||||
scopeDescriptor.getScopeChain(),
|
||||
keyPath,
|
||||
options
|
||||
)
|
||||
|
||||
if result?
|
||||
result
|
||||
else if legacyScopeDescriptor = @getLegacyScopeDescriptor(scopeDescriptor)
|
||||
@scopedSettingsStore.getPropertyValue(
|
||||
legacyScopeDescriptor.getScopeChain(),
|
||||
keyPath,
|
||||
options
|
||||
)
|
||||
|
||||
observeScopedKeyPath: (scope, keyPath, callback) ->
|
||||
callback(@get(keyPath, {scope}))
|
||||
@@ -1160,6 +1191,13 @@ class Config
|
||||
oldValue = newValue
|
||||
callback(event)
|
||||
|
||||
getLegacyScopeDescriptor: (scopeDescriptor) ->
|
||||
legacyAlias = @legacyScopeAliases[scopeDescriptor.scopes[0]]
|
||||
if legacyAlias
|
||||
scopes = scopeDescriptor.scopes.slice()
|
||||
scopes[0] = legacyAlias
|
||||
new ScopeDescriptor({scopes})
|
||||
|
||||
# Base schema enforcers. These will coerce raw input into the specified type,
|
||||
# and will throw an error when the value cannot be coerced. Throwing the error
|
||||
# will indicate that the value should not be set.
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
// 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: parseInt(line || 0, 10),
|
||||
initialColumn: parseInt(column || 0, 10),
|
||||
initialLine: getLineColNumber(line),
|
||||
initialColumn: getLineColNumber(column),
|
||||
searchAllPanes: true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -705,7 +705,7 @@ class Cursor extends Model {
|
||||
*/
|
||||
|
||||
getNonWordCharacters () {
|
||||
return this.editor.getNonWordCharacters(this.getScopeDescriptor().getScopesArray())
|
||||
return this.editor.getNonWordCharacters(this.getBufferPosition())
|
||||
}
|
||||
|
||||
changePosition (options, fn) {
|
||||
|
||||
@@ -34,7 +34,7 @@ export default class DeserializerManager {
|
||||
// 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
|
||||
// {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++) {
|
||||
|
||||
@@ -327,12 +327,15 @@ module.exports = class Dock {
|
||||
// Include all panels that are closer to the edge than the dock in our calculations.
|
||||
switch (this.location) {
|
||||
case 'right':
|
||||
if (!this.isVisible()) bounds.left = bounds.right - 2
|
||||
bounds.right = Number.POSITIVE_INFINITY
|
||||
break
|
||||
case 'bottom':
|
||||
if (!this.isVisible()) bounds.top = bounds.bottom - 1
|
||||
bounds.bottom = Number.POSITIVE_INFINITY
|
||||
break
|
||||
case 'left':
|
||||
if (!this.isVisible()) bounds.right = bounds.left + 2
|
||||
bounds.left = Number.NEGATIVE_INFINITY
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1,28 +1,165 @@
|
||||
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 Grim = require('grim')
|
||||
const {Point, Range} = require('text-buffer')
|
||||
|
||||
const PathSplitRegex = new RegExp('[/.]')
|
||||
const GRAMMAR_TYPE_BONUS = 1000
|
||||
const PATH_SPLIT_REGEX = new RegExp('[/.]')
|
||||
|
||||
// Extended: Syntax class holding the grammars used for tokenizing.
|
||||
// Extended: This class holds 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 {
|
||||
class GrammarRegistry {
|
||||
constructor ({config} = {}) {
|
||||
super({maxTokensPerLine: 100, maxLineLength: 1000})
|
||||
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)
|
||||
}
|
||||
|
||||
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()
|
||||
languageId = this.normalizeLanguageId(languageId)
|
||||
|
||||
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.scopeName !== buffer.getLanguageMode().getLanguageId()) {
|
||||
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 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.scopeName !== buffer.getLanguageMode().getLanguageId()) {
|
||||
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(result.grammar, buffer))
|
||||
}
|
||||
}
|
||||
|
||||
languageModeForGrammarAndBuffer (grammar, buffer) {
|
||||
if (grammar instanceof TreeSitterGrammar) {
|
||||
return new TreeSitterLanguageMode({grammar, buffer, config: this.config})
|
||||
} 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
|
||||
@@ -39,39 +176,44 @@ class GrammarRegistry extends FirstMate.GrammarRegistry {
|
||||
selectGrammarWithScore (filePath, fileContents) {
|
||||
let bestMatch = null
|
||||
let highestScore = -Infinity
|
||||
for (let grammar of this.grammars) {
|
||||
this.forEachGrammar(grammar => {
|
||||
const score = this.getGrammarScore(grammar, filePath, fileContents)
|
||||
if ((score > highestScore) || (bestMatch == null)) {
|
||||
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)) {
|
||||
if (contents == null && fs.isFileSync(filePath)) {
|
||||
contents = fs.readFileSync(filePath, 'utf8')
|
||||
}
|
||||
|
||||
let score = this.getGrammarPathScore(grammar, filePath)
|
||||
if ((score > 0) && !grammar.bundledPackage) {
|
||||
if (score > 0 && !grammar.bundledPackage) {
|
||||
score += 0.125
|
||||
}
|
||||
if (this.grammarMatchesContents(grammar, contents)) {
|
||||
score += 0.25
|
||||
}
|
||||
|
||||
if (score > 0 && this.isGrammarPreferredType(grammar)) {
|
||||
score += GRAMMAR_TYPE_BONUS
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
getGrammarPathScore (grammar, filePath) {
|
||||
if (!filePath) { return -1 }
|
||||
if (!filePath) return -1
|
||||
if (process.platform === 'win32') { filePath = filePath.replace(/\\/g, '/') }
|
||||
|
||||
const pathComponents = filePath.toLowerCase().split(PathSplitRegex)
|
||||
let pathScore = -1
|
||||
const pathComponents = filePath.toLowerCase().split(PATH_SPLIT_REGEX)
|
||||
let pathScore = 0
|
||||
|
||||
let customFileTypes
|
||||
if (this.config.get('core.customFileTypes')) {
|
||||
@@ -85,7 +227,7 @@ class GrammarRegistry extends FirstMate.GrammarRegistry {
|
||||
|
||||
for (let i = 0; i < fileTypes.length; i++) {
|
||||
const fileType = fileTypes[i]
|
||||
const fileTypeComponents = fileType.toLowerCase().split(PathSplitRegex)
|
||||
const fileTypeComponents = fileType.toLowerCase().split(PATH_SPLIT_REGEX)
|
||||
const pathSuffix = pathComponents.slice(-fileTypeComponents.length)
|
||||
if (_.isEqual(pathSuffix, fileTypeComponents)) {
|
||||
pathScore = Math.max(pathScore, fileType.length)
|
||||
@@ -99,25 +241,48 @@ class GrammarRegistry extends FirstMate.GrammarRegistry {
|
||||
}
|
||||
|
||||
grammarMatchesContents (grammar, contents) {
|
||||
if ((contents == null) || (grammar.firstLineRegex == null)) { return false }
|
||||
if (contents == null) return false
|
||||
|
||||
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
|
||||
if (grammar.contentRegExp) { // TreeSitter grammars
|
||||
return grammar.contentRegExp.test(contents)
|
||||
} else if (grammar.firstLineRegex) { // FirstMate grammars
|
||||
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 lines = contents.split('\n')
|
||||
return grammar.firstLineRegex.testSync(lines.slice(0, numberOfNewlinesInRegex + 1).join('\n'))
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
const lines = contents.split('\n')
|
||||
return grammar.firstLineRegex.testSync(lines.slice(0, numberOfNewlinesInRegex + 1).join('\n'))
|
||||
}
|
||||
|
||||
forEachGrammar (callback) {
|
||||
this.textmateRegistry.grammars.forEach(callback)
|
||||
for (let grammarId in this.treeSitterGrammarsById) {
|
||||
callback(this.treeSitterGrammarsById[grammarId])
|
||||
}
|
||||
}
|
||||
|
||||
grammarForId (languageId) {
|
||||
languageId = this.normalizeLanguageId(languageId)
|
||||
|
||||
return (
|
||||
this.textmateRegistry.grammarForScopeName(languageId) ||
|
||||
this.treeSitterGrammarsById[languageId]
|
||||
)
|
||||
}
|
||||
|
||||
// Deprecated: Get the grammar override for the given file path.
|
||||
@@ -126,46 +291,228 @@ class GrammarRegistry extends FirstMate.GrammarRegistry {
|
||||
//
|
||||
// Returns a {String} such as `"source.js"`.
|
||||
grammarOverrideForPath (filePath) {
|
||||
Grim.deprecate('Use atom.textEditors.getGrammarOverride(editor) instead')
|
||||
|
||||
const editor = getEditorForPath(filePath)
|
||||
if (editor) {
|
||||
return atom.textEditors.getGrammarOverride(editor)
|
||||
}
|
||||
Grim.deprecate('Use buffer.getLanguageMode().getLanguageId() instead')
|
||||
const buffer = atom.project.findBufferForPath(filePath)
|
||||
if (buffer) return this.languageOverridesByBufferId.get(buffer.id)
|
||||
}
|
||||
|
||||
// Deprecated: Set the grammar override for the given file path.
|
||||
//
|
||||
// * `filePath` A non-empty {String} file path.
|
||||
// * `scopeName` A {String} such as `"source.js"`.
|
||||
// * `languageId` A {String} such as `"source.js"`.
|
||||
//
|
||||
// Returns undefined.
|
||||
setGrammarOverrideForPath (filePath, scopeName) {
|
||||
Grim.deprecate('Use atom.textEditors.setGrammarOverride(editor, scopeName) instead')
|
||||
|
||||
const editor = getEditorForPath(filePath)
|
||||
if (editor) {
|
||||
atom.textEditors.setGrammarOverride(editor, scopeName)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: Remove the grammar override for the given file path.
|
||||
// Remove the grammar override for the given file path.
|
||||
//
|
||||
// * `filePath` A {String} file path.
|
||||
//
|
||||
// Returns undefined.
|
||||
clearGrammarOverrideForPath (filePath) {
|
||||
Grim.deprecate('Use atom.textEditors.clearGrammarOverride(editor) instead')
|
||||
Grim.deprecate('Use atom.grammars.autoAssignLanguageMode(buffer) instead')
|
||||
const buffer = atom.project.findBufferForPath(filePath)
|
||||
if (buffer) this.languageOverridesByBufferId.delete(buffer.id)
|
||||
}
|
||||
|
||||
const editor = getEditorForPath(filePath)
|
||||
if (editor) {
|
||||
atom.textEditors.clearGrammarOverride(editor)
|
||||
grammarAddedOrUpdated (grammar) {
|
||||
if (grammar.scopeName && !grammar.id) grammar.id = grammar.scopeName
|
||||
|
||||
this.grammarScoresByBuffer.forEach((score, buffer) => {
|
||||
const languageMode = buffer.getLanguageMode()
|
||||
if (grammar.injectionSelector) {
|
||||
if (languageMode.hasTokenForSelector(grammar.injectionSelector)) {
|
||||
languageMode.retokenizeLines()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const languageOverride = this.languageOverridesByBufferId.get(buffer.id)
|
||||
|
||||
if ((grammar.id === buffer.getLanguageMode().getLanguageId() ||
|
||||
grammar.id === languageOverride)) {
|
||||
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer))
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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) {
|
||||
this.treeSitterGrammarsById[grammar.id] = grammar
|
||||
if (grammar.legacyScopeName) {
|
||||
this.config.addLegacyScopeAlias(grammar.id, grammar.legacyScopeName)
|
||||
this.textMateScopeNamesByTreeSitterLanguageId.set(grammar.id, grammar.legacyScopeName)
|
||||
this.treeSitterLanguageIdsByTextMateScopeName.set(grammar.legacyScopeName, grammar.id)
|
||||
}
|
||||
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.id]
|
||||
if (grammar.legacyScopeName) {
|
||||
this.config.removeLegacyScopeAlias(grammar.id)
|
||||
this.textMateScopeNamesByTreeSitterLanguageId.delete(grammar.id)
|
||||
this.treeSitterLanguageIdsByTextMateScopeName.delete(grammar.legacyScopeName)
|
||||
}
|
||||
} 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(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)
|
||||
}
|
||||
|
||||
isGrammarPreferredType (grammar) {
|
||||
return this.config.get('core.useTreeSitterParsers')
|
||||
? grammar instanceof TreeSitterGrammar
|
||||
: grammar instanceof FirstMate.Grammar
|
||||
}
|
||||
|
||||
normalizeLanguageId (languageId) {
|
||||
if (this.config.get('core.useTreeSitterParsers')) {
|
||||
return this.treeSitterLanguageIdsByTextMateScopeName.get(languageId) || languageId
|
||||
} else {
|
||||
return this.textMateScopeNamesByTreeSitterLanguageId.get(languageId) || languageId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getEditorForPath (filePath) {
|
||||
if (filePath != null) {
|
||||
return atom.workspace.getTextEditors().find(editor => editor.getPath() === filePath)
|
||||
}
|
||||
function getGrammarSelectionContent (buffer) {
|
||||
return buffer.getTextInRange(Range(
|
||||
Point(0, 0),
|
||||
buffer.positionForCharacterIndex(1024)
|
||||
))
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ export class HistoryManager {
|
||||
return this.emitter.on('did-change-projects', callback)
|
||||
}
|
||||
|
||||
didChangeProjects (args) {
|
||||
this.emitter.emit('did-change-projects', args || { reloaded: false })
|
||||
didChangeProjects (args = {reloaded: false}) {
|
||||
this.emitter.emit('did-change-projects', args)
|
||||
}
|
||||
|
||||
async addProject (paths, lastOpened) {
|
||||
@@ -93,7 +93,7 @@ export class HistoryManager {
|
||||
}
|
||||
|
||||
async loadState () {
|
||||
let history = await this.stateStore.load('history-manager')
|
||||
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})
|
||||
|
||||
@@ -67,6 +67,7 @@ global.atom = new AtomEnvironment({
|
||||
enablePersistence: true
|
||||
})
|
||||
|
||||
TextEditor.setScheduler(global.atom.views)
|
||||
global.atom.preloadPackages()
|
||||
|
||||
# Like sands through the hourglass, so are the days of our lives.
|
||||
|
||||
@@ -82,6 +82,7 @@ module.exports = ({blobStore}) ->
|
||||
params.onlyLoadBaseStyleSheets = true unless params.hasOwnProperty("onlyLoadBaseStyleSheets")
|
||||
atomEnvironment = new AtomEnvironment(params)
|
||||
atomEnvironment.initialize(params)
|
||||
TextEditor.setScheduler(atomEnvironment.views)
|
||||
atomEnvironment
|
||||
|
||||
promise = testRunner({
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
{app, Menu} = require 'electron'
|
||||
_ = require 'underscore-plus'
|
||||
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) ->
|
||||
@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.getAllWindows(), (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.commandDetail)
|
||||
item.metadata.windowSpecific = true unless /^application:/.test(item.command, item.commandDetail)
|
||||
@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 Electron to provide nice icons where available.
|
||||
acceleratorForCommand: (command, keystrokesByCommand) ->
|
||||
firstKeystroke = keystrokesByCommand[command]?[0]
|
||||
MenuHelpers.acceleratorForKeystroke(firstKeystroke)
|
||||
225
src/main-process/application-menu.js
Normal file
225
src/main-process/application-menu.js
Normal file
@@ -0,0 +1,225 @@
|
||||
const {app, Menu} = require('electron')
|
||||
const _ = require('underscore-plus')
|
||||
const MenuHelpers = require('../menu-helpers')
|
||||
|
||||
// Used to manage the global application menu.
|
||||
//
|
||||
// It's created by {AtomApplication} upon instantiation and used to add, remove
|
||||
// and maintain the state of all menu items.
|
||||
module.exports =
|
||||
class ApplicationMenu {
|
||||
constructor (version, autoUpdateManager) {
|
||||
this.version = version
|
||||
this.autoUpdateManager = autoUpdateManager
|
||||
this.windowTemplates = new WeakMap()
|
||||
this.setActiveTemplate(this.getDefaultTemplate())
|
||||
this.autoUpdateManager.on('state-changed', state => this.showUpdateMenuItem(state))
|
||||
}
|
||||
|
||||
// Public: Updates the entire menu with the given keybindings.
|
||||
//
|
||||
// window - The BrowserWindow this menu template is associated with.
|
||||
// template - The Object which describes the menu to display.
|
||||
// keystrokesByCommand - An Object where the keys are commands and the values
|
||||
// are Arrays containing the keystroke.
|
||||
update (window, template, keystrokesByCommand) {
|
||||
this.translateTemplate(template, keystrokesByCommand)
|
||||
this.substituteVersion(template)
|
||||
this.windowTemplates.set(window, template)
|
||||
if (window === this.lastFocusedWindow) return this.setActiveTemplate(template)
|
||||
}
|
||||
|
||||
setActiveTemplate (template) {
|
||||
if (!_.isEqual(template, this.activeTemplate)) {
|
||||
this.activeTemplate = template
|
||||
this.menu = Menu.buildFromTemplate(_.deepClone(template))
|
||||
Menu.setApplicationMenu(this.menu)
|
||||
}
|
||||
|
||||
return this.showUpdateMenuItem(this.autoUpdateManager.getState())
|
||||
}
|
||||
|
||||
// Register a BrowserWindow with this application menu.
|
||||
addWindow (window) {
|
||||
if (this.lastFocusedWindow == null) this.lastFocusedWindow = window
|
||||
|
||||
const focusHandler = () => {
|
||||
this.lastFocusedWindow = window
|
||||
const template = this.windowTemplates.get(window)
|
||||
if (template) this.setActiveTemplate(template)
|
||||
}
|
||||
|
||||
window.on('focus', focusHandler)
|
||||
window.once('closed', () => {
|
||||
if (window === this.lastFocusedWindow) this.lastFocusedWindow = null
|
||||
this.windowTemplates.delete(window)
|
||||
window.removeListener('focus', focusHandler)
|
||||
})
|
||||
|
||||
this.enableWindowSpecificItems(true)
|
||||
}
|
||||
|
||||
// Flattens the given menu and submenu items into an single Array.
|
||||
//
|
||||
// menu - A complete menu configuration object for atom-shell's menu API.
|
||||
//
|
||||
// Returns an Array of native menu items.
|
||||
flattenMenuItems (menu) {
|
||||
const object = menu.items || {}
|
||||
let items = []
|
||||
for (let index in object) {
|
||||
const item = object[index]
|
||||
items.push(item)
|
||||
if (item.submenu) items = items.concat(this.flattenMenuItems(item.submenu))
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// Flattens the given menu template into an single Array.
|
||||
//
|
||||
// template - An object describing the menu item.
|
||||
//
|
||||
// Returns an Array of native menu items.
|
||||
flattenMenuTemplate (template) {
|
||||
let items = []
|
||||
for (let item of template) {
|
||||
items.push(item)
|
||||
if (item.submenu) items = items.concat(this.flattenMenuTemplate(item.submenu))
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// Public: Used to make all window related menu items are active.
|
||||
//
|
||||
// enable - If true enables all window specific items, if false disables all
|
||||
// window specific items.
|
||||
enableWindowSpecificItems (enable) {
|
||||
for (let item of this.flattenMenuItems(this.menu)) {
|
||||
if (item.metadata && item.metadata.windowSpecific) item.enabled = enable
|
||||
}
|
||||
}
|
||||
|
||||
// Replaces VERSION with the current version.
|
||||
substituteVersion (template) {
|
||||
let item = this.flattenMenuTemplate(template).find(({label}) => label === 'VERSION')
|
||||
if (item) item.label = `Version ${this.version}`
|
||||
}
|
||||
|
||||
// Sets the proper visible state the update menu items
|
||||
showUpdateMenuItem (state) {
|
||||
const items = this.flattenMenuItems(this.menu)
|
||||
const checkForUpdateItem = items.find(({label}) => label === 'Check for Update')
|
||||
const checkingForUpdateItem = items.find(({label}) => label === 'Checking for Update')
|
||||
const downloadingUpdateItem = items.find(({label}) => label === 'Downloading Update')
|
||||
const installUpdateItem = items.find(({label}) => label === 'Restart and Install Update')
|
||||
|
||||
if (!checkForUpdateItem || !checkingForUpdateItem ||
|
||||
!downloadingUpdateItem || !installUpdateItem) return
|
||||
|
||||
checkForUpdateItem.visible = false
|
||||
checkingForUpdateItem.visible = false
|
||||
downloadingUpdateItem.visible = false
|
||||
installUpdateItem.visible = false
|
||||
|
||||
switch (state) {
|
||||
case 'idle':
|
||||
case 'error':
|
||||
case 'no-update-available':
|
||||
checkForUpdateItem.visible = true
|
||||
break
|
||||
case 'checking':
|
||||
checkingForUpdateItem.visible = true
|
||||
break
|
||||
case 'downloading':
|
||||
downloadingUpdateItem.visible = true
|
||||
break
|
||||
case 'update-available':
|
||||
installUpdateItem.visible = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Default list of menu items.
|
||||
//
|
||||
// Returns an Array of menu item Objects.
|
||||
getDefaultTemplate () {
|
||||
return [{
|
||||
label: 'Atom',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Check for Update',
|
||||
metadata: {autoUpdate: true}
|
||||
},
|
||||
{
|
||||
label: 'Reload',
|
||||
accelerator: 'Command+R',
|
||||
click: () => {
|
||||
const window = this.focusedWindow()
|
||||
if (window) window.reload()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Close Window',
|
||||
accelerator: 'Command+Shift+W',
|
||||
click: () => {
|
||||
const window = this.focusedWindow()
|
||||
if (window) window.close()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Toggle Dev Tools',
|
||||
accelerator: 'Command+Alt+I',
|
||||
click: () => {
|
||||
const window = this.focusedWindow()
|
||||
if (window) window.toggleDevTools()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Quit',
|
||||
accelerator: 'Command+Q',
|
||||
click: () => app.quit()
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
|
||||
focusedWindow () {
|
||||
return global.atomApplication.getAllWindows().find(window => window.isFocused())
|
||||
}
|
||||
|
||||
// Combines a menu template with the appropriate keystroke.
|
||||
//
|
||||
// template - An Object conforming to atom-shell's menu api but lacking
|
||||
// accelerator and click properties.
|
||||
// keystrokesByCommand - An Object where the keys are commands and the values
|
||||
// are Arrays containing the keystroke.
|
||||
//
|
||||
// Returns a complete menu configuration object for atom-shell's menu API.
|
||||
translateTemplate (template, keystrokesByCommand) {
|
||||
template.forEach(item => {
|
||||
if (item.metadata == null) item.metadata = {}
|
||||
if (item.command) {
|
||||
item.accelerator = this.acceleratorForCommand(item.command, keystrokesByCommand)
|
||||
item.click = () => global.atomApplication.sendCommand(item.command, item.commandDetail)
|
||||
if (!/^application:/.test(item.command, item.commandDetail)) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,917 +0,0 @@
|
||||
AtomWindow = require './atom-window'
|
||||
ApplicationMenu = require './application-menu'
|
||||
AtomProtocolHandler = require './atom-protocol-handler'
|
||||
AutoUpdateManager = require './auto-update-manager'
|
||||
StorageFolder = require '../storage-folder'
|
||||
Config = require '../config'
|
||||
FileRecoveryService = require './file-recovery-service'
|
||||
ipcHelpers = require '../ipc-helpers'
|
||||
{BrowserWindow, Menu, app, dialog, ipcMain, shell, screen} = require 'electron'
|
||||
{CompositeDisposable, Disposable} = require 'event-kit'
|
||||
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
|
||||
ConfigSchema = require '../config-schema'
|
||||
|
||||
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
|
||||
Object.assign @prototype, EventEmitter.prototype
|
||||
|
||||
# Public: The entry point into the Atom application.
|
||||
@open: (options) ->
|
||||
unless options.socketPath?
|
||||
if process.platform is 'win32'
|
||||
userNameSafe = new Buffer(process.env.USERNAME).toString('base64')
|
||||
options.socketPath = "\\\\.\\pipe\\atom-#{options.version}-#{userNameSafe}-#{process.arch}-sock"
|
||||
else
|
||||
options.socketPath = path.join(os.tmpdir(), "atom-#{options.version}-#{process.env.USER}.sock")
|
||||
|
||||
# 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 or options.benchmark or options.benchmarkTest
|
||||
new AtomApplication(options).initialize(options)
|
||||
return
|
||||
|
||||
client = net.connect {path: options.socketPath}, ->
|
||||
client.write JSON.stringify(options), ->
|
||||
client.end()
|
||||
app.quit()
|
||||
|
||||
client.on 'error', -> new AtomApplication(options).initialize(options)
|
||||
|
||||
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, @logFile, @userDataDir} = options
|
||||
@socketPath = null if options.test or options.benchmark or options.benchmarkTest
|
||||
@pidsToOpenWindows = {}
|
||||
@windowStack = new WindowStack()
|
||||
|
||||
@config = new Config({enablePersistence: true})
|
||||
@config.setSchema null, {type: 'object', properties: _.clone(ConfigSchema)}
|
||||
ConfigSchema.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.'
|
||||
}
|
||||
@config.initialize({configDirPath: process.env.ATOM_HOME, @resourcePath, projectHomeSchema: ConfigSchema.projectHome})
|
||||
@config.load()
|
||||
@fileRecoveryService = new FileRecoveryService(path.join(process.env.ATOM_HOME, "recovery"))
|
||||
@storageFolder = new StorageFolder(process.env.ATOM_HOME)
|
||||
@autoUpdateManager = new AutoUpdateManager(
|
||||
@version,
|
||||
options.test or options.benchmark or options.benchmarkTest,
|
||||
@config
|
||||
)
|
||||
|
||||
@disposable = new CompositeDisposable
|
||||
@handleEvents()
|
||||
|
||||
# This stuff was previously done in the constructor, but we want to be able to construct this object
|
||||
# for testing purposes without booting up the world. As you add tests, feel free to move instantiation
|
||||
# of these various sub-objects into the constructor, but you'll need to remove the side-effects they
|
||||
# perform during their construction, adding an initialize method that you call here.
|
||||
initialize: (options) ->
|
||||
global.atomApplication = this
|
||||
|
||||
# DEPRECATED: This can be removed at some point (added in 1.13)
|
||||
# It converts `useCustomTitleBar: true` to `titleBar: "custom"`
|
||||
if process.platform is 'darwin' and @config.get('core.useCustomTitleBar')
|
||||
@config.unset('core.useCustomTitleBar')
|
||||
@config.set('core.titleBar', 'custom')
|
||||
|
||||
@config.onDidChange 'core.titleBar', @promptForRestart.bind(this)
|
||||
|
||||
process.nextTick => @autoUpdateManager.initialize()
|
||||
@applicationMenu = new ApplicationMenu(@version, @autoUpdateManager)
|
||||
@atomProtocolHandler = new AtomProtocolHandler(@resourcePath, @safeMode)
|
||||
|
||||
@listenForArgumentsFromNewProcess()
|
||||
@setupDockMenu()
|
||||
|
||||
@launch(options)
|
||||
|
||||
destroy: ->
|
||||
windowsClosePromises = @getAllWindows().map (window) ->
|
||||
window.close()
|
||||
window.closedPromise
|
||||
Promise.all(windowsClosePromises).then(=> @disposable.dispose())
|
||||
|
||||
launch: (options) ->
|
||||
if options.test or options.benchmark or options.benchmarkTest
|
||||
@openWithOptions(options)
|
||||
else if options.pathsToOpen?.length > 0 or options.urlsToOpen?.length > 0
|
||||
if @config.get('core.restorePreviousWindowsOnStart') is 'always'
|
||||
@loadState(_.deepClone(options))
|
||||
@openWithOptions(options)
|
||||
else
|
||||
@loadState(options) or @openPath(options)
|
||||
|
||||
openWithOptions: (options) ->
|
||||
{
|
||||
initialPaths, pathsToOpen, executedFrom, urlsToOpen, benchmark,
|
||||
benchmarkTest, test, pidToKillWhenClosed, devMode, safeMode, newWindow,
|
||||
logFile, profileStartup, timeout, clearWindowState, addToLastWindow, env
|
||||
} = options
|
||||
|
||||
app.focus()
|
||||
|
||||
if test
|
||||
@runTests({
|
||||
headless: true, devMode, @resourcePath, executedFrom, pathsToOpen,
|
||||
logFile, timeout, env
|
||||
})
|
||||
else if benchmark or benchmarkTest
|
||||
@runBenchmarks({headless: true, test: benchmarkTest, @resourcePath, executedFrom, pathsToOpen, timeout, env})
|
||||
else if pathsToOpen.length > 0
|
||||
@openPaths({
|
||||
initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow,
|
||||
devMode, safeMode, profileStartup, clearWindowState, addToLastWindow, env
|
||||
})
|
||||
else if urlsToOpen.length > 0
|
||||
for urlToOpen in urlsToOpen
|
||||
@openUrl({urlToOpen, devMode, safeMode, env})
|
||||
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) ->
|
||||
@windowStack.removeWindow(window)
|
||||
if @getAllWindows().length is 0
|
||||
@applicationMenu?.enableWindowSpecificItems(false)
|
||||
if process.platform in ['win32', 'linux']
|
||||
app.quit()
|
||||
return
|
||||
@saveState(true) unless window.isSpec
|
||||
|
||||
# Public: Adds the {AtomWindow} to the global window list.
|
||||
addWindow: (window) ->
|
||||
@windowStack.addWindow(window)
|
||||
@applicationMenu?.addWindow(window.browserWindow)
|
||||
window.once 'window:loaded', =>
|
||||
@autoUpdateManager?.emitUpdateAvailableEvent(window)
|
||||
|
||||
unless window.isSpec
|
||||
focusHandler = => @windowStack.touch(window)
|
||||
blurHandler = => @saveState(false)
|
||||
window.browserWindow.on 'focus', focusHandler
|
||||
window.browserWindow.on 'blur', blurHandler
|
||||
window.browserWindow.once 'closed', =>
|
||||
@windowStack.removeWindow(window)
|
||||
window.browserWindow.removeListener 'focus', focusHandler
|
||||
window.browserWindow.removeListener 'blur', blurHandler
|
||||
window.browserWindow.webContents.once 'did-finish-load', => @saveState(false)
|
||||
|
||||
getAllWindows: =>
|
||||
@windowStack.all().slice()
|
||||
|
||||
getLastFocusedWindow: (predicate) =>
|
||||
@windowStack.getLastFocusedWindow(predicate)
|
||||
|
||||
# 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) =>
|
||||
data = ''
|
||||
connection.on 'data', (chunk) ->
|
||||
data = data + chunk
|
||||
|
||||
connection.on 'end', =>
|
||||
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'
|
||||
|
||||
# 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-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('http://flight-manual.atom.io/')
|
||||
@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#reporting-bugs')
|
||||
@on 'application:search-issues', -> shell.openExternal('https://github.com/search?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'))
|
||||
|
||||
@disposable.add ipcHelpers.on app, 'before-quit', (event) =>
|
||||
resolveBeforeQuitPromise = null
|
||||
@lastBeforeQuitPromise = new Promise((resolve) -> resolveBeforeQuitPromise = resolve)
|
||||
if @quitting
|
||||
resolveBeforeQuitPromise()
|
||||
else
|
||||
event.preventDefault()
|
||||
@quitting = true
|
||||
windowUnloadPromises = @getAllWindows().map((window) -> window.prepareToUnload())
|
||||
Promise.all(windowUnloadPromises).then((windowUnloadedResults) ->
|
||||
didUnloadAllWindows = windowUnloadedResults.every((didUnloadWindow) -> didUnloadWindow)
|
||||
app.quit() if didUnloadAllWindows
|
||||
resolveBeforeQuitPromise()
|
||||
)
|
||||
|
||||
@disposable.add ipcHelpers.on app, 'will-quit', =>
|
||||
@killAllProcesses()
|
||||
@deleteSocketFile()
|
||||
|
||||
@disposable.add ipcHelpers.on app, 'open-file', (event, pathToOpen) =>
|
||||
event.preventDefault()
|
||||
@openPath({pathToOpen})
|
||||
|
||||
@disposable.add ipcHelpers.on app, 'open-url', (event, urlToOpen) =>
|
||||
event.preventDefault()
|
||||
@openUrl({urlToOpen, @devMode, @safeMode})
|
||||
|
||||
@disposable.add ipcHelpers.on app, 'activate', (event, hasVisibleWindows) =>
|
||||
unless hasVisibleWindows
|
||||
event?.preventDefault()
|
||||
@emit('application:new-window')
|
||||
|
||||
@disposable.add ipcHelpers.on ipcMain, 'restart-application', =>
|
||||
@restart()
|
||||
|
||||
@disposable.add ipcHelpers.on ipcMain, 'resolve-proxy', (event, requestId, url) ->
|
||||
event.sender.session.resolveProxy url, (proxy) ->
|
||||
unless event.sender.isDestroyed()
|
||||
event.sender.send('did-resolve-proxy', requestId, proxy)
|
||||
|
||||
@disposable.add ipcHelpers.on ipcMain, 'did-change-history-manager', (event) =>
|
||||
for atomWindow in @getAllWindows()
|
||||
webContents = atomWindow.browserWindow.webContents
|
||||
if webContents isnt event.sender
|
||||
webContents.send('did-change-history-manager')
|
||||
|
||||
# A request from the associated render process to open a new render process.
|
||||
@disposable.add ipcHelpers.on ipcMain, 'open', (event, options) =>
|
||||
window = @atomWindowForEvent(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(this, @fileRecoveryService, options)
|
||||
else
|
||||
@promptForPathToOpen('all', {window})
|
||||
|
||||
@disposable.add ipcHelpers.on ipcMain, 'update-application-menu', (event, template, keystrokesByCommand) =>
|
||||
win = BrowserWindow.fromWebContents(event.sender)
|
||||
@applicationMenu?.update(win, template, keystrokesByCommand)
|
||||
|
||||
@disposable.add ipcHelpers.on ipcMain, 'run-package-specs', (event, packageSpecPath) =>
|
||||
@runTests({resourcePath: @devResourcePath, pathsToOpen: [packageSpecPath], headless: false})
|
||||
|
||||
@disposable.add ipcHelpers.on ipcMain, 'run-benchmarks', (event, benchmarksPath) =>
|
||||
@runBenchmarks({resourcePath: @devResourcePath, pathsToOpen: [benchmarksPath], headless: false, test: false})
|
||||
|
||||
@disposable.add ipcHelpers.on ipcMain, 'command', (event, command) =>
|
||||
@emit(command)
|
||||
|
||||
@disposable.add ipcHelpers.on ipcMain, 'open-command', (event, command, args...) =>
|
||||
defaultPath = args[0] if args.length > 0
|
||||
switch command
|
||||
when 'application:open' then @promptForPathToOpen('all', getLoadSettings(), defaultPath)
|
||||
when 'application:open-file' then @promptForPathToOpen('file', getLoadSettings(), defaultPath)
|
||||
when 'application:open-folder' then @promptForPathToOpen('folder', getLoadSettings(), defaultPath)
|
||||
else console.log "Invalid open-command received: " + command
|
||||
|
||||
@disposable.add ipcHelpers.on ipcMain, 'window-command', (event, command, args...) ->
|
||||
win = BrowserWindow.fromWebContents(event.sender)
|
||||
win.emit(command, args...)
|
||||
|
||||
@disposable.add ipcHelpers.respondTo 'window-method', (browserWindow, method, args...) =>
|
||||
@atomWindowForBrowserWindow(browserWindow)?[method](args...)
|
||||
|
||||
@disposable.add ipcHelpers.on ipcMain, 'pick-folder', (event, responseChannel) =>
|
||||
@promptForPath "folder", (selectedPaths) ->
|
||||
event.sender.send(responseChannel, selectedPaths)
|
||||
|
||||
@disposable.add ipcHelpers.respondTo 'set-window-size', (win, width, height) ->
|
||||
win.setSize(width, height)
|
||||
|
||||
@disposable.add ipcHelpers.respondTo 'set-window-position', (win, x, y) ->
|
||||
win.setPosition(x, y)
|
||||
|
||||
@disposable.add ipcHelpers.respondTo 'center-window', (win) ->
|
||||
win.center()
|
||||
|
||||
@disposable.add ipcHelpers.respondTo 'focus-window', (win) ->
|
||||
win.focus()
|
||||
|
||||
@disposable.add ipcHelpers.respondTo 'show-window', (win) ->
|
||||
win.show()
|
||||
|
||||
@disposable.add ipcHelpers.respondTo 'hide-window', (win) ->
|
||||
win.hide()
|
||||
|
||||
@disposable.add ipcHelpers.respondTo 'get-temporary-window-state', (win) ->
|
||||
win.temporaryState
|
||||
|
||||
@disposable.add ipcHelpers.respondTo 'set-temporary-window-state', (win, state) ->
|
||||
win.temporaryState = state
|
||||
|
||||
clipboard = require '../safe-clipboard'
|
||||
@disposable.add ipcHelpers.on ipcMain, 'write-text-to-selection-clipboard', (event, selectedText) ->
|
||||
clipboard.writeText(selectedText, 'selection')
|
||||
|
||||
@disposable.add ipcHelpers.on ipcMain, 'write-to-stdout', (event, output) ->
|
||||
process.stdout.write(output)
|
||||
|
||||
@disposable.add ipcHelpers.on ipcMain, 'write-to-stderr', (event, output) ->
|
||||
process.stderr.write(output)
|
||||
|
||||
@disposable.add ipcHelpers.on ipcMain, 'add-recent-document', (event, filename) ->
|
||||
app.addRecentDocument(filename)
|
||||
|
||||
@disposable.add ipcHelpers.on ipcMain, 'execute-javascript-in-dev-tools', (event, code) ->
|
||||
event.sender.devToolsWebContents?.executeJavaScript(code)
|
||||
|
||||
@disposable.add ipcHelpers.on ipcMain, 'get-auto-update-manager-state', (event) =>
|
||||
event.returnValue = @autoUpdateManager.getState()
|
||||
|
||||
@disposable.add ipcHelpers.on ipcMain, 'get-auto-update-manager-error', (event) =>
|
||||
event.returnValue = @autoUpdateManager.getErrorMessage()
|
||||
|
||||
@disposable.add ipcHelpers.on ipcMain, 'will-save-path', (event, path) =>
|
||||
@fileRecoveryService.willSavePath(@atomWindowForEvent(event), path)
|
||||
event.returnValue = true
|
||||
|
||||
@disposable.add ipcHelpers.on ipcMain, 'did-save-path', (event, path) =>
|
||||
@fileRecoveryService.didSavePath(@atomWindowForEvent(event), path)
|
||||
event.returnValue = true
|
||||
|
||||
@disposable.add ipcHelpers.on ipcMain, 'did-change-paths', =>
|
||||
@saveState(false)
|
||||
|
||||
@disposable.add(@disableZoomOnDisplayChange())
|
||||
|
||||
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 macOS 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 @getAllWindows(), (atomWindow) ->
|
||||
atomWindow.devMode is devMode and atomWindow.containsPaths(pathsToOpen)
|
||||
|
||||
# Returns the {AtomWindow} for the given ipcMain event.
|
||||
atomWindowForEvent: ({sender}) ->
|
||||
@atomWindowForBrowserWindow(BrowserWindow.fromWebContents(sender))
|
||||
|
||||
atomWindowForBrowserWindow: (browserWindow) ->
|
||||
@getAllWindows().find((atomWindow) -> atomWindow.browserWindow is browserWindow)
|
||||
|
||||
# Public: Returns the currently focused {AtomWindow} or undefined if none.
|
||||
focusedWindow: ->
|
||||
_.find @getAllWindows(), (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() ? @getLastFocusedWindow())?.isMaximized()
|
||||
dimensions = (@focusedWindow() ? @getLastFocusedWindow())?.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}={}) ->
|
||||
if not pathsToOpen? or pathsToOpen.length is 0
|
||||
return
|
||||
env = process.env unless 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 ? @getLastFocusedWindow()
|
||||
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()
|
||||
openedWindow.replaceEnvironment(env)
|
||||
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(this, @fileRecoveryService, {initialPaths, locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState, env})
|
||||
openedWindow.focus()
|
||||
@windowStack.addWindow(openedWindow)
|
||||
|
||||
if pidToKillWhenClosed?
|
||||
@pidsToOpenWindows[pidToKillWhenClosed] = openedWindow
|
||||
|
||||
openedWindow.browserWindow.once 'closed', =>
|
||||
@killProcessForWindow(openedWindow)
|
||||
|
||||
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 @getAllWindows()
|
||||
unless window.isSpec
|
||||
states.push({initialPaths: window.representedDirectoryPaths})
|
||||
states.reverse()
|
||||
if states.length > 0 or allowEmpty
|
||||
@storageFolder.storeSync('application.json', states)
|
||||
@emit('application:did-save-state')
|
||||
|
||||
loadState: (options) ->
|
||||
if (@config.get('core.restorePreviousWindowsOnStart') in ['yes', 'always']) and (states = @storageFolder.load('application.json'))?.length > 0
|
||||
for state in states
|
||||
@openWithOptions(Object.assign(options, {
|
||||
initialPaths: state.initialPaths
|
||||
pathsToOpen: state.initialPaths.filter (directoryPath) -> fs.isDirectorySync(directoryPath)
|
||||
urlsToOpen: []
|
||||
devMode: @devMode
|
||||
safeMode: @safeMode
|
||||
}))
|
||||
else
|
||||
null
|
||||
|
||||
# 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}) ->
|
||||
parsedUrl = url.parse(urlToOpen, true)
|
||||
return unless parsedUrl.protocol is "atom:"
|
||||
|
||||
pack = @findPackageWithName(parsedUrl.host, devMode)
|
||||
if pack?.urlMain
|
||||
@openPackageUrlMain(parsedUrl.host, pack.urlMain, urlToOpen, devMode, safeMode, env)
|
||||
else
|
||||
@openPackageUriHandler(urlToOpen, parsedUrl, devMode, safeMode, env)
|
||||
|
||||
openPackageUriHandler: (url, parsedUrl, devMode, safeMode, env) ->
|
||||
bestWindow = null
|
||||
if parsedUrl.host is 'core'
|
||||
predicate = require('../core-uri-handlers').windowPredicate(parsedUrl)
|
||||
bestWindow = @getLastFocusedWindow (win) ->
|
||||
not win.isSpecWindow() and predicate(win)
|
||||
|
||||
bestWindow ?= @getLastFocusedWindow (win) -> not win.isSpecWindow()
|
||||
if bestWindow?
|
||||
bestWindow.sendURIMessage url
|
||||
bestWindow.focus()
|
||||
else
|
||||
resourcePath = @resourcePath
|
||||
if devMode
|
||||
try
|
||||
windowInitializationScript = require.resolve(path.join(@devResourcePath, 'src', 'initialize-application-window'))
|
||||
resourcePath = @devResourcePath
|
||||
|
||||
windowInitializationScript ?= require.resolve('../initialize-application-window')
|
||||
windowDimensions = @getDimensionsForNewWindow()
|
||||
win = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env})
|
||||
@windowStack.addWindow(win)
|
||||
win.on 'window:loaded', ->
|
||||
win.sendURIMessage url
|
||||
|
||||
findPackageWithName: (packageName, devMode) ->
|
||||
_.find @getPackageManager(devMode).getAvailablePackageMetadata(), ({name}) -> name is packageName
|
||||
|
||||
openPackageUrlMain: (packageName, packageUrlMain, urlToOpen, devMode, safeMode, env) ->
|
||||
packagePath = @getPackageManager(devMode).resolvePackagePath(packageName)
|
||||
windowInitializationScript = path.resolve(packagePath, packageUrlMain)
|
||||
windowDimensions = @getDimensionsForNewWindow()
|
||||
new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env})
|
||||
|
||||
getPackageManager: (devMode) ->
|
||||
unless @packages?
|
||||
PackageManager = require '../package-manager'
|
||||
@packages = new PackageManager({})
|
||||
@packages.initialize
|
||||
configDirPath: process.env.ATOM_HOME
|
||||
devMode: devMode
|
||||
resourcePath: @resourcePath
|
||||
|
||||
@packages
|
||||
|
||||
|
||||
# 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(this, @fileRecoveryService, {windowInitializationScript, resourcePath, headless, isSpec, devMode, testRunnerPath, legacyTestRunnerPath, testPaths, logFile, safeMode, env})
|
||||
|
||||
runBenchmarks: ({headless, test, resourcePath, executedFrom, pathsToOpen, env}) ->
|
||||
if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath)
|
||||
resourcePath = @resourcePath
|
||||
|
||||
try
|
||||
windowInitializationScript = require.resolve(path.resolve(@devResourcePath, 'src', 'initialize-benchmark-window'))
|
||||
catch error
|
||||
windowInitializationScript = require.resolve(path.resolve(__dirname, '..', '..', 'src', 'initialize-benchmark-window'))
|
||||
|
||||
benchmarkPaths = []
|
||||
if pathsToOpen?
|
||||
for pathToOpen in pathsToOpen
|
||||
benchmarkPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen)))
|
||||
|
||||
if benchmarkPaths.length is 0
|
||||
process.stderr.write 'Error: Specify at least one benchmark path.\n\n'
|
||||
process.exit(1)
|
||||
|
||||
devMode = true
|
||||
isSpec = true
|
||||
safeMode = false
|
||||
new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, resourcePath, headless, test, isSpec, devMode, benchmarkPaths, 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 macOS.
|
||||
# :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.
|
||||
# :path - An optional String which controls the default path to which the
|
||||
# file dialog opens.
|
||||
promptForPathToOpen: (type, {devMode, safeMode, window}, path=null) ->
|
||||
@promptForPath type, ((pathsToOpen) =>
|
||||
@openPaths({pathsToOpen, devMode, safeMode, window})), path
|
||||
|
||||
promptForPath: (type, callback, path) ->
|
||||
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 macOS. 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'
|
||||
|
||||
# File dialog defaults to project directory of currently active editor
|
||||
if path?
|
||||
openOptions.defaultPath = path
|
||||
|
||||
dialog.showOpenDialog(parentWindow, openOptions, callback)
|
||||
|
||||
promptForRestart: ->
|
||||
chosen = dialog.showMessageBox BrowserWindow.getFocusedWindow(),
|
||||
type: 'warning'
|
||||
title: 'Restart required'
|
||||
message: "You will need to restart Atom for this change to take effect."
|
||||
buttons: ['Restart Atom', 'Cancel']
|
||||
if chosen is 0
|
||||
@restart()
|
||||
|
||||
restart: ->
|
||||
args = []
|
||||
args.push("--safe") if @safeMode
|
||||
args.push("--log-file=#{@logFile}") if @logFile?
|
||||
args.push("--socket-path=#{@socketPath}") if @socketPath?
|
||||
args.push("--user-data-dir=#{@userDataDir}") if @userDataDir?
|
||||
if @devMode
|
||||
args.push('--dev')
|
||||
args.push("--resource-path=#{@resourcePath}")
|
||||
app.relaunch({args})
|
||||
app.quit()
|
||||
|
||||
disableZoomOnDisplayChange: ->
|
||||
outerCallback = =>
|
||||
for window in @getAllWindows()
|
||||
window.disableZoom()
|
||||
|
||||
# Set the limits every time a display is added or removed, otherwise the
|
||||
# configuration gets reset to the default, which allows zooming the
|
||||
# webframe.
|
||||
screen.on('display-added', outerCallback)
|
||||
screen.on('display-removed', outerCallback)
|
||||
new Disposable ->
|
||||
screen.removeListener('display-added', outerCallback)
|
||||
screen.removeListener('display-removed', outerCallback)
|
||||
|
||||
class WindowStack
|
||||
constructor: (@windows = []) ->
|
||||
|
||||
addWindow: (window) =>
|
||||
@removeWindow(window)
|
||||
@windows.unshift(window)
|
||||
|
||||
touch: (window) =>
|
||||
@addWindow(window)
|
||||
|
||||
removeWindow: (window) =>
|
||||
currentIndex = @windows.indexOf(window)
|
||||
@windows.splice(currentIndex, 1) if currentIndex > -1
|
||||
|
||||
getLastFocusedWindow: (predicate) =>
|
||||
predicate ?= (win) -> true
|
||||
@windows.find(predicate)
|
||||
|
||||
all: =>
|
||||
@windows
|
||||
1376
src/main-process/atom-application.js
Normal file
1376
src/main-process/atom-application.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,43 +0,0 @@
|
||||
{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)
|
||||
54
src/main-process/atom-protocol-handler.js
Normal file
54
src/main-process/atom-protocol-handler.js
Normal file
@@ -0,0 +1,54 @@
|
||||
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(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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,323 +0,0 @@
|
||||
{BrowserWindow, app, dialog, ipcMain} = require 'electron'
|
||||
path = require 'path'
|
||||
fs = require 'fs'
|
||||
url = require 'url'
|
||||
{EventEmitter} = require 'events'
|
||||
|
||||
module.exports =
|
||||
class AtomWindow
|
||||
Object.assign @prototype, EventEmitter.prototype
|
||||
|
||||
@iconPath: path.resolve(__dirname, '..', '..', 'resources', 'atom.png')
|
||||
@includeShellLoadTime: true
|
||||
|
||||
browserWindow: null
|
||||
loaded: null
|
||||
isSpec: null
|
||||
|
||||
constructor: (@atomApplication, @fileRecoveryService, settings={}) ->
|
||||
{@resourcePath, pathToOpen, locationsToOpen, @isSpec, @headless, @safeMode, @devMode} = settings
|
||||
locationsToOpen ?= [{pathToOpen}] if pathToOpen
|
||||
locationsToOpen ?= []
|
||||
|
||||
@loadedPromise = new Promise((@resolveLoadedPromise) =>)
|
||||
@closedPromise = new Promise((@resolveClosedPromise) =>)
|
||||
|
||||
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: not @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 is 'linux'
|
||||
options.icon = @constructor.iconPath
|
||||
|
||||
if @shouldAddCustomTitleBar()
|
||||
options.titleBarStyle = 'hidden'
|
||||
|
||||
if @shouldAddCustomInsetTitleBar()
|
||||
options.titleBarStyle = 'hidden-inset'
|
||||
|
||||
if @shouldHideTitleBar()
|
||||
options.frame = false
|
||||
|
||||
@browserWindow = new BrowserWindow(options)
|
||||
@handleEvents()
|
||||
|
||||
@loadSettings = Object.assign({}, 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
|
||||
stat = fs.statSyncNoException(pathToOpen) or null
|
||||
if stat?.isDirectory()
|
||||
pathToOpen
|
||||
else
|
||||
parentDirectory = path.dirname(pathToOpen)
|
||||
if stat?.isFile() or fs.existsSync(parentDirectory)
|
||||
parentDirectory
|
||||
else
|
||||
pathToOpen
|
||||
@loadSettings.initialPaths.sort()
|
||||
|
||||
# Only send to the first non-spec window created
|
||||
if @constructor.includeShellLoadTime and not @isSpec
|
||||
@constructor.includeShellLoadTime = false
|
||||
@loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime
|
||||
|
||||
@representedDirectoryPaths = @loadSettings.initialPaths
|
||||
@env = @loadSettings.env if @loadSettings.env?
|
||||
|
||||
@browserWindow.loadSettingsJSON = JSON.stringify(@loadSettings)
|
||||
|
||||
@browserWindow.on 'window:loaded', =>
|
||||
@disableZoom()
|
||||
@emit 'window:loaded'
|
||||
@resolveLoadedPromise()
|
||||
|
||||
@browserWindow.on 'window:locations-opened', =>
|
||||
@emit 'window:locations-opened'
|
||||
|
||||
@browserWindow.on 'enter-full-screen', =>
|
||||
@browserWindow.webContents.send('did-enter-full-screen')
|
||||
|
||||
@browserWindow.on 'leave-full-screen', =>
|
||||
@browserWindow.webContents.send('did-leave-full-screen')
|
||||
|
||||
@browserWindow.loadURL url.format
|
||||
protocol: 'file'
|
||||
pathname: "#{@resourcePath}/static/index.html"
|
||||
slashes: true
|
||||
|
||||
@browserWindow.showSaveDialog = @showSaveDialog.bind(this)
|
||||
|
||||
@browserWindow.focusOnWebView() if @isSpec
|
||||
@browserWindow.temporaryState = {windowDimensions} if windowDimensions?
|
||||
|
||||
hasPathToOpen = not (locationsToOpen.length is 1 and not locationsToOpen[0].pathToOpen?)
|
||||
@openLocations(locationsToOpen) if hasPathToOpen and not @isSpecWindow()
|
||||
|
||||
@atomApplication.addWindow(this)
|
||||
|
||||
hasProjectPath: -> @representedDirectoryPaths.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) ->
|
||||
@representedDirectoryPaths.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', (event) =>
|
||||
unless @atomApplication.quitting or @unloading
|
||||
event.preventDefault()
|
||||
@unloading = true
|
||||
@atomApplication.saveState(false)
|
||||
@prepareToUnload().then (result) =>
|
||||
@close() if result
|
||||
|
||||
@browserWindow.on 'closed', =>
|
||||
@fileRecoveryService.didCloseWindow(this)
|
||||
@atomApplication.removeWindow(this)
|
||||
@resolveClosedPromise()
|
||||
|
||||
@browserWindow.on 'unresponsive', =>
|
||||
return if @isSpec
|
||||
|
||||
chosen = dialog.showMessageBox @browserWindow,
|
||||
type: 'warning'
|
||||
buttons: ['Force 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', =>
|
||||
if @headless
|
||||
console.log "Renderer process crashed, exiting"
|
||||
@atomApplication.exit(100)
|
||||
return
|
||||
|
||||
@fileRecoveryService.didCrashWindow(this)
|
||||
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
|
||||
# Spec window's web view should always have focus
|
||||
@browserWindow.on 'blur', =>
|
||||
@browserWindow.focusOnWebView()
|
||||
|
||||
prepareToUnload: ->
|
||||
if @isSpecWindow()
|
||||
return Promise.resolve(true)
|
||||
@lastPrepareToUnloadPromise = new Promise (resolve) =>
|
||||
callback = (event, result) =>
|
||||
if BrowserWindow.fromWebContents(event.sender) is @browserWindow
|
||||
ipcMain.removeListener('did-prepare-to-unload', callback)
|
||||
unless result
|
||||
@unloading = false
|
||||
@atomApplication.quitting = false
|
||||
resolve(result)
|
||||
ipcMain.on('did-prepare-to-unload', callback)
|
||||
@browserWindow.webContents.send('prepare-to-unload')
|
||||
|
||||
openPath: (pathToOpen, initialLine, initialColumn) ->
|
||||
@openLocations([{pathToOpen, initialLine, initialColumn}])
|
||||
|
||||
openLocations: (locationsToOpen) ->
|
||||
@loadedPromise.then => @sendMessage 'open-locations', locationsToOpen
|
||||
|
||||
replaceEnvironment: (env) ->
|
||||
@browserWindow.webContents.send 'environment', env
|
||||
|
||||
sendMessage: (message, detail) ->
|
||||
@browserWindow.webContents.send 'message', message, detail
|
||||
|
||||
sendCommand: (command, args...) ->
|
||||
if @isSpecWindow()
|
||||
unless @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 @atomApplication.sendCommandToFirstResponder(command)
|
||||
@sendCommandToBrowserWindow(command, args...)
|
||||
|
||||
sendURIMessage: (uri) ->
|
||||
@browserWindow.webContents.send 'uri-message', uri
|
||||
|
||||
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}
|
||||
|
||||
shouldAddCustomTitleBar: ->
|
||||
not @isSpec and
|
||||
process.platform is 'darwin' and
|
||||
@atomApplication.config.get('core.titleBar') is 'custom'
|
||||
|
||||
shouldAddCustomInsetTitleBar: ->
|
||||
not @isSpec and
|
||||
process.platform is 'darwin' and
|
||||
@atomApplication.config.get('core.titleBar') is 'custom-inset'
|
||||
|
||||
shouldHideTitleBar: ->
|
||||
not @isSpec and
|
||||
process.platform is 'darwin' and
|
||||
@atomApplication.config.get('core.titleBar') is 'hidden'
|
||||
|
||||
close: -> @browserWindow.close()
|
||||
|
||||
focus: -> @browserWindow.focus()
|
||||
|
||||
minimize: -> @browserWindow.minimize()
|
||||
|
||||
maximize: -> @browserWindow.maximize()
|
||||
|
||||
unmaximize: -> @browserWindow.unmaximize()
|
||||
|
||||
restore: -> @browserWindow.restore()
|
||||
|
||||
setFullScreen: (fullScreen) -> @browserWindow.setFullScreen(fullScreen)
|
||||
|
||||
setAutoHideMenuBar: (autoHideMenuBar) -> @browserWindow.setAutoHideMenuBar(autoHideMenuBar)
|
||||
|
||||
handlesAtomCommands: ->
|
||||
not @isSpecWindow() and @isWebViewFocused()
|
||||
|
||||
isFocused: -> @browserWindow.isFocused()
|
||||
|
||||
isMaximized: -> @browserWindow.isMaximized()
|
||||
|
||||
isMinimized: -> @browserWindow.isMinimized()
|
||||
|
||||
isWebViewFocused: -> @browserWindow.isWebViewFocused()
|
||||
|
||||
isSpecWindow: -> @isSpec
|
||||
|
||||
reload: ->
|
||||
@loadedPromise = new Promise((@resolveLoadedPromise) =>)
|
||||
@prepareToUnload().then (result) =>
|
||||
@browserWindow.reload() if result
|
||||
@loadedPromise
|
||||
|
||||
showSaveDialog: (params) ->
|
||||
params = Object.assign({
|
||||
title: 'Save File',
|
||||
defaultPath: @representedDirectoryPaths[0]
|
||||
}, params)
|
||||
dialog.showSaveDialog(@browserWindow, params)
|
||||
|
||||
toggleDevTools: -> @browserWindow.toggleDevTools()
|
||||
|
||||
openDevTools: -> @browserWindow.openDevTools()
|
||||
|
||||
closeDevTools: -> @browserWindow.closeDevTools()
|
||||
|
||||
setDocumentEdited: (documentEdited) -> @browserWindow.setDocumentEdited(documentEdited)
|
||||
|
||||
setRepresentedFilename: (representedFilename) -> @browserWindow.setRepresentedFilename(representedFilename)
|
||||
|
||||
setRepresentedDirectoryPaths: (@representedDirectoryPaths) ->
|
||||
@representedDirectoryPaths.sort()
|
||||
@loadSettings.initialPaths = @representedDirectoryPaths
|
||||
@browserWindow.loadSettingsJSON = JSON.stringify(@loadSettings)
|
||||
@atomApplication.saveState()
|
||||
|
||||
copy: -> @browserWindow.copy()
|
||||
|
||||
disableZoom: ->
|
||||
@browserWindow.webContents.setVisualZoomLevelLimits(1, 1)
|
||||
432
src/main-process/atom-window.js
Normal file
432
src/main-process/atom-window.js
Normal file
@@ -0,0 +1,432 @@
|
||||
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 = 'hidden-inset'
|
||||
if (this.shouldHideTitleBar()) options.frame = false
|
||||
this.browserWindow = new BrowserWindow(options)
|
||||
|
||||
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} of locationsToOpen) {
|
||||
if (!pathToOpen) continue
|
||||
const stat = fs.statSyncNoException(pathToOpen) || null
|
||||
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.loadSettingsJSON = JSON.stringify(this.loadSettings)
|
||||
|
||||
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
|
||||
const stat = fs.statSyncNoException(pathToCheck)
|
||||
if (stat && stat.isDirectory()) return false
|
||||
|
||||
return this.representedDirectoryPaths.some(projectPath =>
|
||||
pathToCheck === projectPath || pathToCheck.startsWith(path.join(projectPath, path.sep))
|
||||
)
|
||||
}
|
||||
|
||||
handleEvents () {
|
||||
this.browserWindow.on('close', async event => {
|
||||
if (!this.atomApplication.quitting && !this.unloading) {
|
||||
event.preventDefault()
|
||||
this.unloading = true
|
||||
this.atomApplication.saveState(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
|
||||
const chosen = dialog.showMessageBox(this.browserWindow, {
|
||||
type: 'warning',
|
||||
buttons: ['Force 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?'
|
||||
})
|
||||
if (chosen === 0) this.browserWindow.destroy()
|
||||
})
|
||||
|
||||
this.browserWindow.webContents.on('crashed', () => {
|
||||
if (this.headless) {
|
||||
console.log('Renderer process crashed, exiting')
|
||||
this.atomApplication.exit(100)
|
||||
return
|
||||
}
|
||||
|
||||
this.fileRecoveryService.didCrashWindow(this)
|
||||
const chosen = dialog.showMessageBox(this.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) {
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
this.browserWindow.loadSettingsJSON = JSON.stringify(this.loadSettings)
|
||||
return this.atomApplication.saveState()
|
||||
}
|
||||
|
||||
didClosePathWithWaitSession (path) {
|
||||
this.atomApplication.windowDidClosePathWithWaitSession(this, path)
|
||||
}
|
||||
|
||||
copy () {
|
||||
return this.browserWindow.copy()
|
||||
}
|
||||
|
||||
disableZoom () {
|
||||
return this.browserWindow.webContents.setVisualZoomLevelLimits(1, 1)
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ class ContextMenu
|
||||
constructor: (template, @atomWindow) ->
|
||||
template = @createClickHandlers(template)
|
||||
menu = Menu.buildFromTemplate(template)
|
||||
menu.popup(@atomWindow.browserWindow)
|
||||
menu.popup(@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
|
||||
|
||||
@@ -27,6 +27,15 @@ class NotificationManager {
|
||||
return this.emitter.on('did-add-notification', callback)
|
||||
}
|
||||
|
||||
// Public: Invoke the given callback after the notifications have been cleared.
|
||||
//
|
||||
// * `callback` {Function} to be called after the notifications are cleared.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidClearNotifications (callback) {
|
||||
return this.emitter.on('did-clear-notifications', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Adding Notifications
|
||||
*/
|
||||
@@ -200,7 +209,9 @@ class NotificationManager {
|
||||
Section: Managing Notifications
|
||||
*/
|
||||
|
||||
// Public: Clear all the notifications.
|
||||
clear () {
|
||||
this.notifications = []
|
||||
this.emitter.emit('did-clear-notifications')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,848 +0,0 @@
|
||||
path = require 'path'
|
||||
|
||||
_ = require 'underscore-plus'
|
||||
async = require 'async'
|
||||
CSON = require 'season'
|
||||
fs = require 'fs-plus'
|
||||
{Emitter, CompositeDisposable} = require 'event-kit'
|
||||
|
||||
CompileCache = require './compile-cache'
|
||||
ModuleCache = require './module-cache'
|
||||
ScopedProperties = require './scoped-properties'
|
||||
BufferedProcess = require './buffered-process'
|
||||
|
||||
# Extended: Loads and activates a package's main module and resources such as
|
||||
# stylesheets, keymaps, grammar, editor properties, and menus.
|
||||
module.exports =
|
||||
class Package
|
||||
keymaps: null
|
||||
menus: null
|
||||
stylesheets: null
|
||||
stylesheetDisposables: null
|
||||
grammars: null
|
||||
settings: null
|
||||
mainModulePath: null
|
||||
resolvedMainModulePath: false
|
||||
mainModule: null
|
||||
mainInitialized: false
|
||||
mainActivated: false
|
||||
|
||||
###
|
||||
Section: Construction
|
||||
###
|
||||
|
||||
constructor: (params) ->
|
||||
{
|
||||
@path, @metadata, @bundledPackage, @preloadedPackage, @packageManager, @config, @styleManager, @commandRegistry,
|
||||
@keymapManager, @notificationManager, @grammarRegistry, @themeManager,
|
||||
@menuManager, @contextMenuManager, @deserializerManager, @viewRegistry
|
||||
} = params
|
||||
|
||||
@emitter = new Emitter
|
||||
@metadata ?= @packageManager.loadPackageMetadata(@path)
|
||||
@bundledPackage ?= @packageManager.isBundledPackagePath(@path)
|
||||
@name = @metadata?.name ? params.name ? path.basename(@path)
|
||||
@reset()
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
###
|
||||
|
||||
# Essential: Invoke the given callback when all packages have been activated.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDeactivate: (callback) ->
|
||||
@emitter.on 'did-deactivate', callback
|
||||
|
||||
###
|
||||
Section: Instance Methods
|
||||
###
|
||||
|
||||
enable: ->
|
||||
@config.removeAtKeyPath('core.disabledPackages', @name)
|
||||
|
||||
disable: ->
|
||||
@config.pushAtKeyPath('core.disabledPackages', @name)
|
||||
|
||||
isTheme: ->
|
||||
@metadata?.theme?
|
||||
|
||||
measure: (key, fn) ->
|
||||
startTime = Date.now()
|
||||
value = fn()
|
||||
@[key] = Date.now() - startTime
|
||||
value
|
||||
|
||||
getType: -> 'atom'
|
||||
|
||||
getStyleSheetPriority: -> 0
|
||||
|
||||
preload: ->
|
||||
@loadKeymaps()
|
||||
@loadMenus()
|
||||
@registerDeserializerMethods()
|
||||
@activateCoreStartupServices()
|
||||
@registerURIHandler()
|
||||
@configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata()
|
||||
@requireMainModule()
|
||||
@settingsPromise = @loadSettings()
|
||||
|
||||
@activationDisposables = new CompositeDisposable
|
||||
@activateKeymaps()
|
||||
@activateMenus()
|
||||
settings.activate() for settings in @settings
|
||||
@settingsActivated = true
|
||||
|
||||
finishLoading: ->
|
||||
@measure 'loadTime', =>
|
||||
@path = path.join(@packageManager.resourcePath, @path)
|
||||
ModuleCache.add(@path, @metadata)
|
||||
|
||||
@loadStylesheets()
|
||||
# Unfortunately some packages are accessing `@mainModulePath`, so we need
|
||||
# to compute that variable eagerly also for preloaded packages.
|
||||
@getMainModulePath()
|
||||
|
||||
load: ->
|
||||
@measure 'loadTime', =>
|
||||
try
|
||||
ModuleCache.add(@path, @metadata)
|
||||
|
||||
@loadKeymaps()
|
||||
@loadMenus()
|
||||
@loadStylesheets()
|
||||
@registerDeserializerMethods()
|
||||
@activateCoreStartupServices()
|
||||
@registerURIHandler()
|
||||
@registerTranspilerConfig()
|
||||
@configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata()
|
||||
@settingsPromise = @loadSettings()
|
||||
if @shouldRequireMainModuleOnLoad() and not @mainModule?
|
||||
@requireMainModule()
|
||||
catch error
|
||||
@handleError("Failed to load the #{@name} package", error)
|
||||
this
|
||||
|
||||
unload: ->
|
||||
@unregisterTranspilerConfig()
|
||||
|
||||
shouldRequireMainModuleOnLoad: ->
|
||||
not (
|
||||
@metadata.deserializers? or
|
||||
@metadata.viewProviders? or
|
||||
@metadata.configSchema? or
|
||||
@activationShouldBeDeferred() or
|
||||
localStorage.getItem(@getCanDeferMainModuleRequireStorageKey()) is 'true'
|
||||
)
|
||||
|
||||
reset: ->
|
||||
@stylesheets = []
|
||||
@keymaps = []
|
||||
@menus = []
|
||||
@grammars = []
|
||||
@settings = []
|
||||
@mainInitialized = false
|
||||
@mainActivated = false
|
||||
|
||||
initializeIfNeeded: ->
|
||||
return if @mainInitialized
|
||||
@measure 'initializeTime', =>
|
||||
try
|
||||
# The main module's `initialize()` method is guaranteed to be called
|
||||
# before its `activate()`. This gives you a chance to handle the
|
||||
# serialized package state before the package's derserializers and view
|
||||
# providers are used.
|
||||
@requireMainModule() unless @mainModule?
|
||||
@mainModule.initialize?(@packageManager.getPackageState(@name) ? {})
|
||||
@mainInitialized = true
|
||||
catch error
|
||||
@handleError("Failed to initialize the #{@name} package", error)
|
||||
return
|
||||
|
||||
activate: ->
|
||||
@grammarsPromise ?= @loadGrammars()
|
||||
@activationPromise ?=
|
||||
new Promise (resolve, reject) =>
|
||||
@resolveActivationPromise = resolve
|
||||
@measure 'activateTime', =>
|
||||
try
|
||||
@activateResources()
|
||||
if @activationShouldBeDeferred()
|
||||
@subscribeToDeferredActivation()
|
||||
else
|
||||
@activateNow()
|
||||
catch error
|
||||
@handleError("Failed to activate the #{@name} package", error)
|
||||
|
||||
Promise.all([@grammarsPromise, @settingsPromise, @activationPromise])
|
||||
|
||||
activateNow: ->
|
||||
try
|
||||
@requireMainModule() unless @mainModule?
|
||||
@configSchemaRegisteredOnActivate = @registerConfigSchemaFromMainModule()
|
||||
@registerViewProviders()
|
||||
@activateStylesheets()
|
||||
if @mainModule? and not @mainActivated
|
||||
@initializeIfNeeded()
|
||||
@mainModule.activateConfig?()
|
||||
@mainModule.activate?(@packageManager.getPackageState(@name) ? {})
|
||||
@mainActivated = true
|
||||
@activateServices()
|
||||
@activationCommandSubscriptions?.dispose()
|
||||
@activationHookSubscriptions?.dispose()
|
||||
catch error
|
||||
@handleError("Failed to activate the #{@name} package", error)
|
||||
|
||||
@resolveActivationPromise?()
|
||||
|
||||
registerConfigSchemaFromMetadata: ->
|
||||
if configSchema = @metadata.configSchema
|
||||
@config.setSchema @name, {type: 'object', properties: configSchema}
|
||||
true
|
||||
else
|
||||
false
|
||||
|
||||
registerConfigSchemaFromMainModule: ->
|
||||
if @mainModule? and not @configSchemaRegisteredOnLoad
|
||||
if @mainModule.config? and typeof @mainModule.config is 'object'
|
||||
@config.setSchema @name, {type: 'object', properties: @mainModule.config}
|
||||
return true
|
||||
false
|
||||
|
||||
# TODO: Remove. Settings view calls this method currently.
|
||||
activateConfig: ->
|
||||
return if @configSchemaRegisteredOnLoad
|
||||
@requireMainModule()
|
||||
@registerConfigSchemaFromMainModule()
|
||||
|
||||
activateStylesheets: ->
|
||||
return if @stylesheetsActivated
|
||||
|
||||
@stylesheetDisposables = new CompositeDisposable
|
||||
|
||||
priority = @getStyleSheetPriority()
|
||||
for [sourcePath, source] in @stylesheets
|
||||
if match = path.basename(sourcePath).match(/[^.]*\.([^.]*)\./)
|
||||
context = match[1]
|
||||
else if @metadata.theme is 'syntax'
|
||||
context = 'atom-text-editor'
|
||||
else
|
||||
context = undefined
|
||||
|
||||
@stylesheetDisposables.add(
|
||||
@styleManager.addStyleSheet(
|
||||
source,
|
||||
{
|
||||
sourcePath,
|
||||
priority,
|
||||
context,
|
||||
skipDeprecatedSelectorsTransformation: @bundledPackage
|
||||
}
|
||||
)
|
||||
)
|
||||
@stylesheetsActivated = true
|
||||
|
||||
activateResources: ->
|
||||
@activationDisposables ?= new CompositeDisposable
|
||||
|
||||
keymapIsDisabled = _.include(@config.get("core.packagesWithKeymapsDisabled") ? [], @name)
|
||||
if keymapIsDisabled
|
||||
@deactivateKeymaps()
|
||||
else unless @keymapActivated
|
||||
@activateKeymaps()
|
||||
|
||||
unless @menusActivated
|
||||
@activateMenus()
|
||||
|
||||
unless @grammarsActivated
|
||||
grammar.activate() for grammar in @grammars
|
||||
@grammarsActivated = true
|
||||
|
||||
unless @settingsActivated
|
||||
settings.activate() for settings in @settings
|
||||
@settingsActivated = true
|
||||
|
||||
activateKeymaps: ->
|
||||
return if @keymapActivated
|
||||
|
||||
@keymapDisposables = new CompositeDisposable()
|
||||
|
||||
validateSelectors = not @preloadedPackage
|
||||
@keymapDisposables.add(@keymapManager.add(keymapPath, map, 0, validateSelectors)) for [keymapPath, map] in @keymaps
|
||||
@menuManager.update()
|
||||
|
||||
@keymapActivated = true
|
||||
|
||||
deactivateKeymaps: ->
|
||||
return if not @keymapActivated
|
||||
|
||||
@keymapDisposables?.dispose()
|
||||
@menuManager.update()
|
||||
|
||||
@keymapActivated = false
|
||||
|
||||
hasKeymaps: ->
|
||||
for [path, map] in @keymaps
|
||||
if map.length > 0
|
||||
return true
|
||||
false
|
||||
|
||||
activateMenus: ->
|
||||
validateSelectors = not @preloadedPackage
|
||||
for [menuPath, map] in @menus when map['context-menu']?
|
||||
try
|
||||
itemsBySelector = map['context-menu']
|
||||
@activationDisposables.add(@contextMenuManager.add(itemsBySelector, validateSelectors))
|
||||
catch error
|
||||
if error.code is 'EBADSELECTOR'
|
||||
error.message += " in #{menuPath}"
|
||||
error.stack += "\n at #{menuPath}:1:1"
|
||||
throw error
|
||||
|
||||
for [menuPath, map] in @menus when map['menu']?
|
||||
@activationDisposables.add(@menuManager.add(map['menu']))
|
||||
|
||||
@menusActivated = true
|
||||
|
||||
activateServices: ->
|
||||
for name, {versions} of @metadata.providedServices
|
||||
servicesByVersion = {}
|
||||
for version, methodName of versions
|
||||
if typeof @mainModule[methodName] is 'function'
|
||||
servicesByVersion[version] = @mainModule[methodName]()
|
||||
@activationDisposables.add @packageManager.serviceHub.provide(name, servicesByVersion)
|
||||
|
||||
for name, {versions} of @metadata.consumedServices
|
||||
for version, methodName of versions
|
||||
if typeof @mainModule[methodName] is 'function'
|
||||
@activationDisposables.add @packageManager.serviceHub.consume(name, version, @mainModule[methodName].bind(@mainModule))
|
||||
return
|
||||
|
||||
registerURIHandler: ->
|
||||
handlerConfig = @getURIHandler()
|
||||
if methodName = handlerConfig?.method
|
||||
@uriHandlerSubscription = @packageManager.registerURIHandlerForPackage @name, (args...) =>
|
||||
@handleURI(methodName, args)
|
||||
|
||||
unregisterURIHandler: ->
|
||||
@uriHandlerSubscription?.dispose()
|
||||
|
||||
handleURI: (methodName, args) ->
|
||||
@activate().then => @mainModule[methodName]?.apply(@mainModule, args)
|
||||
@activateNow() unless @mainActivated
|
||||
|
||||
registerTranspilerConfig: ->
|
||||
if @metadata.atomTranspilers
|
||||
CompileCache.addTranspilerConfigForPath(@path, @name, @metadata, @metadata.atomTranspilers)
|
||||
|
||||
unregisterTranspilerConfig: ->
|
||||
if @metadata.atomTranspilers
|
||||
CompileCache.removeTranspilerConfigForPath(@path)
|
||||
|
||||
loadKeymaps: ->
|
||||
if @bundledPackage and @packageManager.packagesCache[@name]?
|
||||
@keymaps = (["core:#{keymapPath}", keymapObject] for keymapPath, keymapObject of @packageManager.packagesCache[@name].keymaps)
|
||||
else
|
||||
@keymaps = @getKeymapPaths().map (keymapPath) -> [keymapPath, CSON.readFileSync(keymapPath, allowDuplicateKeys: false) ? {}]
|
||||
return
|
||||
|
||||
loadMenus: ->
|
||||
if @bundledPackage and @packageManager.packagesCache[@name]?
|
||||
@menus = (["core:#{menuPath}", menuObject] for menuPath, menuObject of @packageManager.packagesCache[@name].menus)
|
||||
else
|
||||
@menus = @getMenuPaths().map (menuPath) -> [menuPath, CSON.readFileSync(menuPath) ? {}]
|
||||
return
|
||||
|
||||
getKeymapPaths: ->
|
||||
keymapsDirPath = path.join(@path, 'keymaps')
|
||||
if @metadata.keymaps
|
||||
@metadata.keymaps.map (name) -> fs.resolve(keymapsDirPath, name, ['json', 'cson', ''])
|
||||
else
|
||||
fs.listSync(keymapsDirPath, ['cson', 'json'])
|
||||
|
||||
getMenuPaths: ->
|
||||
menusDirPath = path.join(@path, 'menus')
|
||||
if @metadata.menus
|
||||
@metadata.menus.map (name) -> fs.resolve(menusDirPath, name, ['json', 'cson', ''])
|
||||
else
|
||||
fs.listSync(menusDirPath, ['cson', 'json'])
|
||||
|
||||
loadStylesheets: ->
|
||||
@stylesheets = @getStylesheetPaths().map (stylesheetPath) =>
|
||||
[stylesheetPath, @themeManager.loadStylesheet(stylesheetPath, true)]
|
||||
|
||||
registerDeserializerMethods: ->
|
||||
if @metadata.deserializers?
|
||||
Object.keys(@metadata.deserializers).forEach (deserializerName) =>
|
||||
methodName = @metadata.deserializers[deserializerName]
|
||||
@deserializerManager.add
|
||||
name: deserializerName,
|
||||
deserialize: (state, atomEnvironment) =>
|
||||
@registerViewProviders()
|
||||
@requireMainModule()
|
||||
@initializeIfNeeded()
|
||||
@mainModule[methodName](state, atomEnvironment)
|
||||
return
|
||||
|
||||
activateCoreStartupServices: ->
|
||||
if directoryProviderService = @metadata.providedServices?['atom.directory-provider']
|
||||
@requireMainModule()
|
||||
servicesByVersion = {}
|
||||
for version, methodName of directoryProviderService.versions
|
||||
if typeof @mainModule[methodName] is 'function'
|
||||
servicesByVersion[version] = @mainModule[methodName]()
|
||||
@packageManager.serviceHub.provide('atom.directory-provider', servicesByVersion)
|
||||
|
||||
registerViewProviders: ->
|
||||
if @metadata.viewProviders? and not @registeredViewProviders
|
||||
@requireMainModule()
|
||||
@metadata.viewProviders.forEach (methodName) =>
|
||||
@viewRegistry.addViewProvider (model) =>
|
||||
@initializeIfNeeded()
|
||||
@mainModule[methodName](model)
|
||||
@registeredViewProviders = true
|
||||
|
||||
getStylesheetsPath: ->
|
||||
path.join(@path, 'styles')
|
||||
|
||||
getStylesheetPaths: ->
|
||||
if @bundledPackage and @packageManager.packagesCache[@name]?.styleSheetPaths?
|
||||
styleSheetPaths = @packageManager.packagesCache[@name].styleSheetPaths
|
||||
styleSheetPaths.map (styleSheetPath) => path.join(@path, styleSheetPath)
|
||||
else
|
||||
stylesheetDirPath = @getStylesheetsPath()
|
||||
if @metadata.mainStyleSheet
|
||||
[fs.resolve(@path, @metadata.mainStyleSheet)]
|
||||
else if @metadata.styleSheets
|
||||
@metadata.styleSheets.map (name) -> fs.resolve(stylesheetDirPath, name, ['css', 'less', ''])
|
||||
else if indexStylesheet = fs.resolve(@path, 'index', ['css', 'less'])
|
||||
[indexStylesheet]
|
||||
else
|
||||
fs.listSync(stylesheetDirPath, ['css', 'less'])
|
||||
|
||||
loadGrammarsSync: ->
|
||||
return if @grammarsLoaded
|
||||
|
||||
if @preloadedPackage and @packageManager.packagesCache[@name]?
|
||||
grammarPaths = @packageManager.packagesCache[@name].grammarPaths
|
||||
else
|
||||
grammarPaths = fs.listSync(path.join(@path, 'grammars'), ['json', 'cson'])
|
||||
|
||||
for grammarPath in grammarPaths
|
||||
if @preloadedPackage and @packageManager.packagesCache[@name]?
|
||||
grammarPath = path.resolve(@packageManager.resourcePath, grammarPath)
|
||||
|
||||
try
|
||||
grammar = @grammarRegistry.readGrammarSync(grammarPath)
|
||||
grammar.packageName = @name
|
||||
grammar.bundledPackage = @bundledPackage
|
||||
@grammars.push(grammar)
|
||||
grammar.activate()
|
||||
catch error
|
||||
console.warn("Failed to load grammar: #{grammarPath}", error.stack ? error)
|
||||
|
||||
@grammarsLoaded = true
|
||||
@grammarsActivated = true
|
||||
|
||||
loadGrammars: ->
|
||||
return Promise.resolve() if @grammarsLoaded
|
||||
|
||||
loadGrammar = (grammarPath, callback) =>
|
||||
if @preloadedPackage
|
||||
grammarPath = path.resolve(@packageManager.resourcePath, grammarPath)
|
||||
|
||||
@grammarRegistry.readGrammar grammarPath, (error, grammar) =>
|
||||
if error?
|
||||
detail = "#{error.message} in #{grammarPath}"
|
||||
stack = "#{error.stack}\n at #{grammarPath}:1:1"
|
||||
@notificationManager.addFatalError("Failed to load a #{@name} package grammar", {stack, detail, packageName: @name, dismissable: true})
|
||||
else
|
||||
grammar.packageName = @name
|
||||
grammar.bundledPackage = @bundledPackage
|
||||
@grammars.push(grammar)
|
||||
grammar.activate() if @grammarsActivated
|
||||
callback()
|
||||
|
||||
new Promise (resolve) =>
|
||||
if @preloadedPackage and @packageManager.packagesCache[@name]?
|
||||
grammarPaths = @packageManager.packagesCache[@name].grammarPaths
|
||||
async.each grammarPaths, loadGrammar, -> resolve()
|
||||
else
|
||||
grammarsDirPath = path.join(@path, 'grammars')
|
||||
fs.exists grammarsDirPath, (grammarsDirExists) ->
|
||||
return resolve() unless grammarsDirExists
|
||||
|
||||
fs.list grammarsDirPath, ['json', 'cson'], (error, grammarPaths=[]) ->
|
||||
async.each grammarPaths, loadGrammar, -> resolve()
|
||||
|
||||
loadSettings: ->
|
||||
@settings = []
|
||||
|
||||
loadSettingsFile = (settingsPath, callback) =>
|
||||
ScopedProperties.load settingsPath, @config, (error, settings) =>
|
||||
if error?
|
||||
detail = "#{error.message} in #{settingsPath}"
|
||||
stack = "#{error.stack}\n at #{settingsPath}:1:1"
|
||||
@notificationManager.addFatalError("Failed to load the #{@name} package settings", {stack, detail, packageName: @name, dismissable: true})
|
||||
else
|
||||
@settings.push(settings)
|
||||
settings.activate() if @settingsActivated
|
||||
callback()
|
||||
|
||||
new Promise (resolve) =>
|
||||
if @preloadedPackage and @packageManager.packagesCache[@name]?
|
||||
for settingsPath, scopedProperties of @packageManager.packagesCache[@name].settings
|
||||
settings = new ScopedProperties("core:#{settingsPath}", scopedProperties ? {}, @config)
|
||||
@settings.push(settings)
|
||||
settings.activate() if @settingsActivated
|
||||
resolve()
|
||||
else
|
||||
settingsDirPath = path.join(@path, 'settings')
|
||||
fs.exists settingsDirPath, (settingsDirExists) ->
|
||||
return resolve() unless settingsDirExists
|
||||
|
||||
fs.list settingsDirPath, ['json', 'cson'], (error, settingsPaths=[]) ->
|
||||
async.each settingsPaths, loadSettingsFile, -> resolve()
|
||||
|
||||
serialize: ->
|
||||
if @mainActivated
|
||||
try
|
||||
@mainModule?.serialize?()
|
||||
catch e
|
||||
console.error "Error serializing package '#{@name}'", e.stack
|
||||
|
||||
deactivate: ->
|
||||
@activationPromise = null
|
||||
@resolveActivationPromise = null
|
||||
@activationCommandSubscriptions?.dispose()
|
||||
@activationHookSubscriptions?.dispose()
|
||||
@configSchemaRegisteredOnActivate = false
|
||||
@unregisterURIHandler()
|
||||
@deactivateResources()
|
||||
@deactivateKeymaps()
|
||||
|
||||
unless @mainActivated
|
||||
@emitter.emit 'did-deactivate'
|
||||
return
|
||||
|
||||
try
|
||||
deactivationResult = @mainModule?.deactivate?()
|
||||
catch e
|
||||
console.error "Error deactivating package '#{@name}'", e.stack
|
||||
|
||||
# We support then-able async promises as well as sync ones from deactivate
|
||||
if typeof deactivationResult?.then is 'function'
|
||||
deactivationResult.then => @afterDeactivation()
|
||||
else
|
||||
@afterDeactivation()
|
||||
|
||||
afterDeactivation: ->
|
||||
try
|
||||
@mainModule?.deactivateConfig?()
|
||||
catch e
|
||||
console.error "Error deactivating package '#{@name}'", e.stack
|
||||
@mainActivated = false
|
||||
@mainInitialized = false
|
||||
@emitter.emit 'did-deactivate'
|
||||
|
||||
deactivateResources: ->
|
||||
grammar.deactivate() for grammar in @grammars
|
||||
settings.deactivate() for settings in @settings
|
||||
@stylesheetDisposables?.dispose()
|
||||
@activationDisposables?.dispose()
|
||||
@keymapDisposables?.dispose()
|
||||
@stylesheetsActivated = false
|
||||
@grammarsActivated = false
|
||||
@settingsActivated = false
|
||||
@menusActivated = false
|
||||
|
||||
reloadStylesheets: ->
|
||||
try
|
||||
@loadStylesheets()
|
||||
catch error
|
||||
@handleError("Failed to reload the #{@name} package stylesheets", error)
|
||||
|
||||
@stylesheetDisposables?.dispose()
|
||||
@stylesheetDisposables = new CompositeDisposable
|
||||
@stylesheetsActivated = false
|
||||
@activateStylesheets()
|
||||
|
||||
requireMainModule: ->
|
||||
if @bundledPackage and @packageManager.packagesCache[@name]?
|
||||
if @packageManager.packagesCache[@name].main?
|
||||
@mainModule = require(@packageManager.packagesCache[@name].main)
|
||||
else if @mainModuleRequired
|
||||
@mainModule
|
||||
else if not @isCompatible()
|
||||
console.warn """
|
||||
Failed to require the main module of '#{@name}' because it requires one or more incompatible native modules (#{_.pluck(@incompatibleModules, 'name').join(', ')}).
|
||||
Run `apm rebuild` in the package directory and restart Atom to resolve.
|
||||
"""
|
||||
return
|
||||
else
|
||||
mainModulePath = @getMainModulePath()
|
||||
if fs.isFileSync(mainModulePath)
|
||||
@mainModuleRequired = true
|
||||
|
||||
previousViewProviderCount = @viewRegistry.getViewProviderCount()
|
||||
previousDeserializerCount = @deserializerManager.getDeserializerCount()
|
||||
@mainModule = require(mainModulePath)
|
||||
if (@viewRegistry.getViewProviderCount() is previousViewProviderCount and
|
||||
@deserializerManager.getDeserializerCount() is previousDeserializerCount)
|
||||
localStorage.setItem(@getCanDeferMainModuleRequireStorageKey(), 'true')
|
||||
|
||||
getMainModulePath: ->
|
||||
return @mainModulePath if @resolvedMainModulePath
|
||||
@resolvedMainModulePath = true
|
||||
|
||||
if @bundledPackage and @packageManager.packagesCache[@name]?
|
||||
if @packageManager.packagesCache[@name].main
|
||||
@mainModulePath = path.resolve(@packageManager.resourcePath, 'static', @packageManager.packagesCache[@name].main)
|
||||
else
|
||||
@mainModulePath = null
|
||||
else
|
||||
mainModulePath =
|
||||
if @metadata.main
|
||||
path.join(@path, @metadata.main)
|
||||
else
|
||||
path.join(@path, 'index')
|
||||
@mainModulePath = fs.resolveExtension(mainModulePath, ["", CompileCache.supportedExtensions...])
|
||||
|
||||
activationShouldBeDeferred: ->
|
||||
@hasActivationCommands() or @hasActivationHooks() or @hasDeferredURIHandler()
|
||||
|
||||
hasActivationHooks: ->
|
||||
@getActivationHooks()?.length > 0
|
||||
|
||||
hasActivationCommands: ->
|
||||
for selector, commands of @getActivationCommands()
|
||||
return true if commands.length > 0
|
||||
false
|
||||
|
||||
hasDeferredURIHandler: ->
|
||||
@getURIHandler() and @getURIHandler().deferActivation isnt false
|
||||
|
||||
subscribeToDeferredActivation: ->
|
||||
@subscribeToActivationCommands()
|
||||
@subscribeToActivationHooks()
|
||||
|
||||
subscribeToActivationCommands: ->
|
||||
@activationCommandSubscriptions = new CompositeDisposable
|
||||
for selector, commands of @getActivationCommands()
|
||||
for command in commands
|
||||
do (selector, command) =>
|
||||
# Add dummy command so it appears in menu.
|
||||
# The real command will be registered on package activation
|
||||
try
|
||||
@activationCommandSubscriptions.add @commandRegistry.add selector, command, ->
|
||||
catch error
|
||||
if error.code is 'EBADSELECTOR'
|
||||
metadataPath = path.join(@path, 'package.json')
|
||||
error.message += " in #{metadataPath}"
|
||||
error.stack += "\n at #{metadataPath}:1:1"
|
||||
throw error
|
||||
|
||||
@activationCommandSubscriptions.add @commandRegistry.onWillDispatch (event) =>
|
||||
return unless event.type is command
|
||||
currentTarget = event.target
|
||||
while currentTarget
|
||||
if currentTarget.webkitMatchesSelector(selector)
|
||||
@activationCommandSubscriptions.dispose()
|
||||
@activateNow()
|
||||
break
|
||||
currentTarget = currentTarget.parentElement
|
||||
return
|
||||
return
|
||||
|
||||
getActivationCommands: ->
|
||||
return @activationCommands if @activationCommands?
|
||||
|
||||
@activationCommands = {}
|
||||
|
||||
if @metadata.activationCommands?
|
||||
for selector, commands of @metadata.activationCommands
|
||||
@activationCommands[selector] ?= []
|
||||
if _.isString(commands)
|
||||
@activationCommands[selector].push(commands)
|
||||
else if _.isArray(commands)
|
||||
@activationCommands[selector].push(commands...)
|
||||
|
||||
@activationCommands
|
||||
|
||||
subscribeToActivationHooks: ->
|
||||
@activationHookSubscriptions = new CompositeDisposable
|
||||
for hook in @getActivationHooks()
|
||||
do (hook) =>
|
||||
@activationHookSubscriptions.add(@packageManager.onDidTriggerActivationHook(hook, => @activateNow())) if hook? and _.isString(hook) and hook.trim().length > 0
|
||||
|
||||
return
|
||||
|
||||
getActivationHooks: ->
|
||||
return @activationHooks if @metadata? and @activationHooks?
|
||||
|
||||
@activationHooks = []
|
||||
|
||||
if @metadata.activationHooks?
|
||||
if _.isArray(@metadata.activationHooks)
|
||||
@activationHooks.push(@metadata.activationHooks...)
|
||||
else if _.isString(@metadata.activationHooks)
|
||||
@activationHooks.push(@metadata.activationHooks)
|
||||
|
||||
@activationHooks = _.uniq(@activationHooks)
|
||||
|
||||
getURIHandler: ->
|
||||
@metadata?.uriHandler
|
||||
|
||||
# Does the given module path contain native code?
|
||||
isNativeModule: (modulePath) ->
|
||||
try
|
||||
fs.listSync(path.join(modulePath, 'build', 'Release'), ['.node']).length > 0
|
||||
catch error
|
||||
false
|
||||
|
||||
# Get an array of all the native modules that this package depends on.
|
||||
#
|
||||
# First try to get this information from
|
||||
# @metadata._atomModuleCache.extensions. If @metadata._atomModuleCache doesn't
|
||||
# exist, recurse through all dependencies.
|
||||
getNativeModuleDependencyPaths: ->
|
||||
nativeModulePaths = []
|
||||
|
||||
if @metadata._atomModuleCache?
|
||||
relativeNativeModuleBindingPaths = @metadata._atomModuleCache.extensions?['.node'] ? []
|
||||
for relativeNativeModuleBindingPath in relativeNativeModuleBindingPaths
|
||||
nativeModulePath = path.join(@path, relativeNativeModuleBindingPath, '..', '..', '..')
|
||||
nativeModulePaths.push(nativeModulePath)
|
||||
return nativeModulePaths
|
||||
|
||||
traversePath = (nodeModulesPath) =>
|
||||
try
|
||||
for modulePath in fs.listSync(nodeModulesPath)
|
||||
nativeModulePaths.push(modulePath) if @isNativeModule(modulePath)
|
||||
traversePath(path.join(modulePath, 'node_modules'))
|
||||
return
|
||||
|
||||
traversePath(path.join(@path, 'node_modules'))
|
||||
nativeModulePaths
|
||||
|
||||
###
|
||||
Section: Native Module Compatibility
|
||||
###
|
||||
|
||||
# Extended: Are all native modules depended on by this package correctly
|
||||
# compiled against the current version of Atom?
|
||||
#
|
||||
# Incompatible packages cannot be activated.
|
||||
#
|
||||
# Returns a {Boolean}, true if compatible, false if incompatible.
|
||||
isCompatible: ->
|
||||
return @compatible if @compatible?
|
||||
|
||||
if @preloadedPackage
|
||||
# Preloaded packages are always considered compatible
|
||||
@compatible = true
|
||||
else if @getMainModulePath()
|
||||
@incompatibleModules = @getIncompatibleNativeModules()
|
||||
@compatible = @incompatibleModules.length is 0 and not @getBuildFailureOutput()?
|
||||
else
|
||||
@compatible = true
|
||||
|
||||
# Extended: Rebuild native modules in this package's dependencies for the
|
||||
# current version of Atom.
|
||||
#
|
||||
# Returns a {Promise} that resolves with an object containing `code`,
|
||||
# `stdout`, and `stderr` properties based on the results of running
|
||||
# `apm rebuild` on the package.
|
||||
rebuild: ->
|
||||
new Promise (resolve) =>
|
||||
@runRebuildProcess (result) =>
|
||||
if result.code is 0
|
||||
global.localStorage.removeItem(@getBuildFailureOutputStorageKey())
|
||||
else
|
||||
@compatible = false
|
||||
global.localStorage.setItem(@getBuildFailureOutputStorageKey(), result.stderr)
|
||||
global.localStorage.setItem(@getIncompatibleNativeModulesStorageKey(), '[]')
|
||||
resolve(result)
|
||||
|
||||
# Extended: If a previous rebuild failed, get the contents of stderr.
|
||||
#
|
||||
# Returns a {String} or null if no previous build failure occurred.
|
||||
getBuildFailureOutput: ->
|
||||
global.localStorage.getItem(@getBuildFailureOutputStorageKey())
|
||||
|
||||
runRebuildProcess: (callback) ->
|
||||
stderr = ''
|
||||
stdout = ''
|
||||
new BufferedProcess({
|
||||
command: @packageManager.getApmPath()
|
||||
args: ['rebuild', '--no-color']
|
||||
options: {cwd: @path}
|
||||
stderr: (output) -> stderr += output
|
||||
stdout: (output) -> stdout += output
|
||||
exit: (code) -> callback({code, stdout, stderr})
|
||||
})
|
||||
|
||||
getBuildFailureOutputStorageKey: ->
|
||||
"installed-packages:#{@name}:#{@metadata.version}:build-error"
|
||||
|
||||
getIncompatibleNativeModulesStorageKey: ->
|
||||
electronVersion = process.versions.electron
|
||||
"installed-packages:#{@name}:#{@metadata.version}:electron-#{electronVersion}:incompatible-native-modules"
|
||||
|
||||
getCanDeferMainModuleRequireStorageKey: ->
|
||||
"installed-packages:#{@name}:#{@metadata.version}:can-defer-main-module-require"
|
||||
|
||||
# Get the incompatible native modules that this package depends on.
|
||||
# This recurses through all dependencies and requires all modules that
|
||||
# contain a `.node` file.
|
||||
#
|
||||
# This information is cached in local storage on a per package/version basis
|
||||
# to minimize the impact on startup time.
|
||||
getIncompatibleNativeModules: ->
|
||||
unless @packageManager.devMode
|
||||
try
|
||||
if arrayAsString = global.localStorage.getItem(@getIncompatibleNativeModulesStorageKey())
|
||||
return JSON.parse(arrayAsString)
|
||||
|
||||
incompatibleNativeModules = []
|
||||
for nativeModulePath in @getNativeModuleDependencyPaths()
|
||||
try
|
||||
require(nativeModulePath)
|
||||
catch error
|
||||
try
|
||||
version = require("#{nativeModulePath}/package.json").version
|
||||
incompatibleNativeModules.push
|
||||
path: nativeModulePath
|
||||
name: path.basename(nativeModulePath)
|
||||
version: version
|
||||
error: error.message
|
||||
|
||||
global.localStorage.setItem(@getIncompatibleNativeModulesStorageKey(), JSON.stringify(incompatibleNativeModules))
|
||||
incompatibleNativeModules
|
||||
|
||||
handleError: (message, error) ->
|
||||
if atom.inSpecMode()
|
||||
throw error
|
||||
|
||||
if error.filename and error.location and (error instanceof SyntaxError)
|
||||
location = "#{error.filename}:#{error.location.first_line + 1}:#{error.location.first_column + 1}"
|
||||
detail = "#{error.message} in #{location}"
|
||||
stack = """
|
||||
SyntaxError: #{error.message}
|
||||
at #{location}
|
||||
"""
|
||||
else if error.less and error.filename and error.column? and error.line?
|
||||
# Less errors
|
||||
location = "#{error.filename}:#{error.line}:#{error.column}"
|
||||
detail = "#{error.message} in #{location}"
|
||||
stack = """
|
||||
LessError: #{error.message}
|
||||
at #{location}
|
||||
"""
|
||||
else
|
||||
detail = error.message
|
||||
stack = error.stack ? error
|
||||
|
||||
@notificationManager.addFatalError(message, {stack, detail, packageName: @name, dismissable: true})
|
||||
1107
src/package.js
Normal file
1107
src/package.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,12 @@ class PaneResizeHandleElement extends HTMLElement
|
||||
@addEventListener 'mousedown', @resizeStarted.bind(this)
|
||||
|
||||
attachedCallback: ->
|
||||
@isHorizontal = @parentElement.classList.contains("horizontal")
|
||||
@classList.add if @isHorizontal then 'horizontal' else 'vertical'
|
||||
# For some reason Chromium 58 is firing the attached callback after the
|
||||
# element has been detached, so we ignore the callback when a parent element
|
||||
# can't be found.
|
||||
if @parentElement
|
||||
@isHorizontal = @parentElement.classList.contains("horizontal")
|
||||
@classList.add if @isHorizontal then 'horizontal' else 'vertical'
|
||||
|
||||
detachedCallback: ->
|
||||
@resizeStopped()
|
||||
|
||||
140
src/pane.js
140
src/pane.js
@@ -790,57 +790,53 @@ class Pane {
|
||||
}
|
||||
|
||||
promptToSaveItem (item, options = {}) {
|
||||
if (typeof item.shouldPromptToSave !== 'function' || !item.shouldPromptToSave(options)) {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
|
||||
let uri
|
||||
if (typeof item.getURI === 'function') {
|
||||
uri = item.getURI()
|
||||
} else if (typeof item.getUri === 'function') {
|
||||
uri = item.getUri()
|
||||
} else {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
|
||||
const title = (typeof item.getTitle === 'function' && item.getTitle()) || uri
|
||||
|
||||
const saveDialog = (saveButtonText, saveFn, message) => {
|
||||
const chosen = this.applicationDelegate.confirm({
|
||||
message,
|
||||
detailedMessage: 'Your changes will be lost if you close this item without saving.',
|
||||
buttons: [saveButtonText, 'Cancel', "&Don't Save"]}
|
||||
)
|
||||
|
||||
switch (chosen) {
|
||||
case 0:
|
||||
return new Promise(resolve => {
|
||||
return saveFn(item, error => {
|
||||
if (error instanceof SaveCancelledError) {
|
||||
resolve(false)
|
||||
} else if (error) {
|
||||
saveDialog(
|
||||
'Save as',
|
||||
this.saveItemAs,
|
||||
`'${title}' could not be saved.\nError: ${this.getMessageForErrorCode(error.code)}`
|
||||
).then(resolve)
|
||||
} else {
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
case 1:
|
||||
return Promise.resolve(false)
|
||||
case 2:
|
||||
return Promise.resolve(true)
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof item.shouldPromptToSave !== 'function' || !item.shouldPromptToSave(options)) {
|
||||
return resolve(true)
|
||||
}
|
||||
}
|
||||
|
||||
return saveDialog(
|
||||
'Save',
|
||||
this.saveItem,
|
||||
`'${title}' has changes, do you want to save them?`
|
||||
)
|
||||
let uri
|
||||
if (typeof item.getURI === 'function') {
|
||||
uri = item.getURI()
|
||||
} else if (typeof item.getUri === 'function') {
|
||||
uri = item.getUri()
|
||||
} else {
|
||||
return resolve(true)
|
||||
}
|
||||
|
||||
const title = (typeof item.getTitle === 'function' && item.getTitle()) || uri
|
||||
|
||||
const saveDialog = (saveButtonText, saveFn, message) => {
|
||||
this.applicationDelegate.confirm({
|
||||
message,
|
||||
detail: 'Your changes will be lost if you close this item without saving.',
|
||||
buttons: [saveButtonText, 'Cancel', "&Don't Save"]
|
||||
}, response => {
|
||||
switch (response) {
|
||||
case 0:
|
||||
return saveFn(item, error => {
|
||||
if (error instanceof SaveCancelledError) {
|
||||
resolve(false)
|
||||
} else if (error) {
|
||||
saveDialog(
|
||||
'Save as',
|
||||
this.saveItemAs,
|
||||
`'${title}' could not be saved.\nError: ${this.getMessageForErrorCode(error.code)}`
|
||||
)
|
||||
} else {
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
case 1:
|
||||
return resolve(false)
|
||||
case 2:
|
||||
return resolve(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
saveDialog('Save', this.saveItem, `'${title}' has changes, do you want to save them?`)
|
||||
})
|
||||
}
|
||||
|
||||
// Public: Save the active item.
|
||||
@@ -908,7 +904,7 @@ class Pane {
|
||||
// after the item is successfully saved, or with the error if it failed.
|
||||
// The return value will be that of `nextAction` or `undefined` if it was not
|
||||
// provided
|
||||
saveItemAs (item, nextAction) {
|
||||
async saveItemAs (item, nextAction) {
|
||||
if (!item) return
|
||||
if (typeof item.saveAs !== 'function') return
|
||||
|
||||
@@ -919,22 +915,34 @@ class Pane {
|
||||
const itemPath = item.getPath()
|
||||
if (itemPath && !saveOptions.defaultPath) saveOptions.defaultPath = itemPath
|
||||
|
||||
const newItemPath = this.applicationDelegate.showSaveDialog(saveOptions)
|
||||
if (newItemPath) {
|
||||
return promisify(() => item.saveAs(newItemPath))
|
||||
.then(() => {
|
||||
if (nextAction) nextAction()
|
||||
})
|
||||
.catch(error => {
|
||||
if (nextAction) {
|
||||
nextAction(error)
|
||||
} else {
|
||||
this.handleSaveError(error, item)
|
||||
}
|
||||
})
|
||||
} else if (nextAction) {
|
||||
return nextAction(new SaveCancelledError('Save Cancelled'))
|
||||
}
|
||||
let resolveSaveDialogPromise = null
|
||||
const saveDialogPromise = new Promise(resolve => { resolveSaveDialogPromise = resolve })
|
||||
this.applicationDelegate.showSaveDialog(saveOptions, newItemPath => {
|
||||
if (newItemPath) {
|
||||
promisify(() => item.saveAs(newItemPath))
|
||||
.then(() => {
|
||||
if (nextAction) {
|
||||
resolveSaveDialogPromise(nextAction())
|
||||
} else {
|
||||
resolveSaveDialogPromise()
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (nextAction) {
|
||||
resolveSaveDialogPromise(nextAction(error))
|
||||
} else {
|
||||
this.handleSaveError(error, item)
|
||||
resolveSaveDialogPromise()
|
||||
}
|
||||
})
|
||||
} else if (nextAction) {
|
||||
resolveSaveDialogPromise(nextAction(new SaveCancelledError('Save Cancelled')))
|
||||
} else {
|
||||
resolveSaveDialogPromise()
|
||||
}
|
||||
})
|
||||
|
||||
return await saveDialogPromise
|
||||
}
|
||||
|
||||
// Public: Save all items.
|
||||
|
||||
@@ -422,7 +422,7 @@ class PathWatcher {
|
||||
// Extended: Return a {Promise} that will resolve when the underlying native watcher is ready to begin sending events.
|
||||
// When testing filesystem watchers, it's important to await this promise before making filesystem changes that you
|
||||
// intend to assert about because there will be a delay between the instantiation of the watcher and the activation
|
||||
// of the underlying OS resources that feed it events.
|
||||
// of the underlying OS resources that feed its events.
|
||||
//
|
||||
// PathWatchers acquired through `watchPath` are already started.
|
||||
//
|
||||
@@ -533,7 +533,7 @@ class PathWatcher {
|
||||
}
|
||||
}
|
||||
|
||||
// Extended: Unsubscribe all subscribers from filesystem events. Native resources will be release asynchronously,
|
||||
// Extended: Unsubscribe all subscribers from filesystem events. Native resources will be released asynchronously,
|
||||
// but this watcher will stop broadcasting events immediately.
|
||||
dispose () {
|
||||
for (const sub of this.changeCallbacks.values()) {
|
||||
|
||||
@@ -2,7 +2,7 @@ const path = require('path')
|
||||
|
||||
const _ = require('underscore-plus')
|
||||
const fs = require('fs-plus')
|
||||
const {Emitter, Disposable} = require('event-kit')
|
||||
const {Emitter, Disposable, CompositeDisposable} = require('event-kit')
|
||||
const TextBuffer = require('text-buffer')
|
||||
const {watchPath} = require('./path-watcher')
|
||||
|
||||
@@ -19,10 +19,12 @@ class Project extends Model {
|
||||
Section: Construction and Destruction
|
||||
*/
|
||||
|
||||
constructor ({notificationManager, packageManager, config, applicationDelegate}) {
|
||||
constructor ({notificationManager, packageManager, config, applicationDelegate, grammarRegistry}) {
|
||||
super()
|
||||
this.notificationManager = notificationManager
|
||||
this.applicationDelegate = applicationDelegate
|
||||
this.grammarRegistry = grammarRegistry
|
||||
|
||||
this.emitter = new Emitter()
|
||||
this.buffers = []
|
||||
this.rootDirectories = []
|
||||
@@ -35,6 +37,7 @@ class Project extends Model {
|
||||
this.watcherPromisesByPath = {}
|
||||
this.retiredBufferIDs = new Set()
|
||||
this.retiredBufferPaths = new Set()
|
||||
this.subscriptions = new CompositeDisposable()
|
||||
this.consumeServices(packageManager)
|
||||
}
|
||||
|
||||
@@ -54,6 +57,9 @@ class Project extends Model {
|
||||
this.emitter.dispose()
|
||||
this.emitter = new Emitter()
|
||||
|
||||
this.subscriptions.dispose()
|
||||
this.subscriptions = new CompositeDisposable()
|
||||
|
||||
for (let buffer of this.buffers) {
|
||||
if (buffer != null) buffer.destroy()
|
||||
}
|
||||
@@ -104,6 +110,7 @@ class Project extends Model {
|
||||
return Promise.all(bufferPromises).then(buffers => {
|
||||
this.buffers = buffers.filter(Boolean)
|
||||
for (let buffer of this.buffers) {
|
||||
this.grammarRegistry.maintainLanguageMode(buffer)
|
||||
this.subscribeToBuffer(buffer)
|
||||
}
|
||||
this.setPaths(state.paths || [], {mustExist: true, exact: true})
|
||||
@@ -211,7 +218,7 @@ class Project extends Model {
|
||||
//
|
||||
// This method will be removed in 2.0 because it does synchronous I/O.
|
||||
// Prefer the following, which evaluates to a {Promise} that resolves to an
|
||||
// {Array} of {Repository} objects:
|
||||
// {Array} of {GitRepository} objects:
|
||||
// ```
|
||||
// Promise.all(atom.project.getDirectories().map(
|
||||
// atom.project.repositoryForDirectory.bind(atom.project)))
|
||||
@@ -222,10 +229,10 @@ class Project extends Model {
|
||||
|
||||
// Public: Get the repository for a given directory asynchronously.
|
||||
//
|
||||
// * `directory` {Directory} for which to get a {Repository}.
|
||||
// * `directory` {Directory} for which to get a {GitRepository}.
|
||||
//
|
||||
// Returns a {Promise} that resolves with either:
|
||||
// * {Repository} if a repository can be created for the given directory
|
||||
// * {GitRepository} if a repository can be created for the given directory
|
||||
// * `null` if no repository can be created for the given directory.
|
||||
repositoryForDirectory (directory) {
|
||||
const pathForDirectory = directory.getRealPathSync()
|
||||
@@ -654,11 +661,8 @@ class Project extends Model {
|
||||
}
|
||||
|
||||
addBuffer (buffer, options = {}) {
|
||||
return this.addBufferAtIndex(buffer, this.buffers.length, options)
|
||||
}
|
||||
|
||||
addBufferAtIndex (buffer, index, options = {}) {
|
||||
this.buffers.splice(index, 0, buffer)
|
||||
this.buffers.push(buffer)
|
||||
this.subscriptions.add(this.grammarRegistry.maintainLanguageMode(buffer))
|
||||
this.subscribeToBuffer(buffer)
|
||||
this.emitter.emit('did-add-buffer', buffer)
|
||||
return buffer
|
||||
|
||||
@@ -12,13 +12,13 @@ class ProtocolHandlerInstaller {
|
||||
}
|
||||
|
||||
isDefaultProtocolClient () {
|
||||
return remote.app.isDefaultProtocolClient('atom', process.execPath, ['--uri-handler'])
|
||||
return remote.app.isDefaultProtocolClient('atom', process.execPath, ['--uri-handler', '--'])
|
||||
}
|
||||
|
||||
setAsDefaultProtocolClient () {
|
||||
// This Electron API is only available on Windows and macOS. There might be some
|
||||
// hacks to make it work on Linux; see https://github.com/electron/electron/issues/6440
|
||||
return this.isSupported() && remote.app.setAsDefaultProtocolClient('atom', process.execPath, ['--uri-handler'])
|
||||
return this.isSupported() && remote.app.setAsDefaultProtocolClient('atom', process.execPath, ['--uri-handler', '--'])
|
||||
}
|
||||
|
||||
initialize (config, notifications) {
|
||||
@@ -26,19 +26,28 @@ class ProtocolHandlerInstaller {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.isDefaultProtocolClient()) {
|
||||
const behaviorWhenNotProtocolClient = config.get(SETTING)
|
||||
switch (behaviorWhenNotProtocolClient) {
|
||||
case PROMPT:
|
||||
const behaviorWhenNotProtocolClient = config.get(SETTING)
|
||||
switch (behaviorWhenNotProtocolClient) {
|
||||
case PROMPT:
|
||||
if (!this.isDefaultProtocolClient()) {
|
||||
this.promptToBecomeProtocolClient(config, notifications)
|
||||
break
|
||||
case ALWAYS:
|
||||
}
|
||||
break
|
||||
case ALWAYS:
|
||||
if (!this.isDefaultProtocolClient()) {
|
||||
this.setAsDefaultProtocolClient()
|
||||
break
|
||||
case NEVER:
|
||||
default:
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
break
|
||||
case NEVER:
|
||||
if (process.platform === 'win32') {
|
||||
// Only win32 supports deregistration
|
||||
const Registry = require('winreg')
|
||||
const commandKey = new Registry({hive: 'HKCR', key: `\\atom`})
|
||||
commandKey.destroy((_err, _val) => { /* no op */ })
|
||||
}
|
||||
break
|
||||
default:
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +72,7 @@ class ProtocolHandlerInstaller {
|
||||
notification = notifications.addInfo('Register as default atom:// URI handler?', {
|
||||
dismissable: true,
|
||||
icon: 'link',
|
||||
description: 'Atom is not currently set as the defaut handler for atom:// URIs. Would you like Atom to handle ' +
|
||||
description: 'Atom is not currently set as the default handler for atom:// URIs. Would you like Atom to handle ' +
|
||||
'atom:// URIs?',
|
||||
buttons: [
|
||||
{
|
||||
|
||||
@@ -160,6 +160,8 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage
|
||||
'editor:select-to-previous-subword-boundary': -> @selectToPreviousSubwordBoundary()
|
||||
'editor:select-to-first-character-of-line': -> @selectToFirstCharacterOfLine()
|
||||
'editor:select-line': -> @selectLinesContainingCursors()
|
||||
'editor:select-larger-syntax-node': -> @selectLargerSyntaxNode()
|
||||
'editor:select-smaller-syntax-node': -> @selectSmallerSyntaxNode()
|
||||
}),
|
||||
false
|
||||
)
|
||||
@@ -219,18 +221,40 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage
|
||||
'editor:toggle-soft-wrap': -> @toggleSoftWrapped()
|
||||
'editor:fold-all': -> @foldAll()
|
||||
'editor:unfold-all': -> @unfoldAll()
|
||||
'editor:fold-current-row': -> @foldCurrentRow()
|
||||
'editor:unfold-current-row': -> @unfoldCurrentRow()
|
||||
'editor:fold-current-row': ->
|
||||
@foldCurrentRow()
|
||||
@scrollToCursorPosition()
|
||||
'editor:unfold-current-row': ->
|
||||
@unfoldCurrentRow()
|
||||
@scrollToCursorPosition()
|
||||
'editor:fold-selection': -> @foldSelectedLines()
|
||||
'editor:fold-at-indent-level-1': -> @foldAllAtIndentLevel(0)
|
||||
'editor:fold-at-indent-level-2': -> @foldAllAtIndentLevel(1)
|
||||
'editor:fold-at-indent-level-3': -> @foldAllAtIndentLevel(2)
|
||||
'editor:fold-at-indent-level-4': -> @foldAllAtIndentLevel(3)
|
||||
'editor:fold-at-indent-level-5': -> @foldAllAtIndentLevel(4)
|
||||
'editor:fold-at-indent-level-6': -> @foldAllAtIndentLevel(5)
|
||||
'editor:fold-at-indent-level-7': -> @foldAllAtIndentLevel(6)
|
||||
'editor:fold-at-indent-level-8': -> @foldAllAtIndentLevel(7)
|
||||
'editor:fold-at-indent-level-9': -> @foldAllAtIndentLevel(8)
|
||||
'editor:fold-at-indent-level-1': ->
|
||||
@foldAllAtIndentLevel(0)
|
||||
@scrollToCursorPosition()
|
||||
'editor:fold-at-indent-level-2': ->
|
||||
@foldAllAtIndentLevel(1)
|
||||
@scrollToCursorPosition()
|
||||
'editor:fold-at-indent-level-3': ->
|
||||
@foldAllAtIndentLevel(2)
|
||||
@scrollToCursorPosition()
|
||||
'editor:fold-at-indent-level-4': ->
|
||||
@foldAllAtIndentLevel(3)
|
||||
@scrollToCursorPosition()
|
||||
'editor:fold-at-indent-level-5': ->
|
||||
@foldAllAtIndentLevel(4)
|
||||
@scrollToCursorPosition()
|
||||
'editor:fold-at-indent-level-6': ->
|
||||
@foldAllAtIndentLevel(5)
|
||||
@scrollToCursorPosition()
|
||||
'editor:fold-at-indent-level-7': ->
|
||||
@foldAllAtIndentLevel(6)
|
||||
@scrollToCursorPosition()
|
||||
'editor:fold-at-indent-level-8': ->
|
||||
@foldAllAtIndentLevel(7)
|
||||
@scrollToCursorPosition()
|
||||
'editor:fold-at-indent-level-9': ->
|
||||
@foldAllAtIndentLevel(8)
|
||||
@scrollToCursorPosition()
|
||||
'editor:log-cursor-scope': -> showCursorScope(@getCursorScope(), notificationManager)
|
||||
'editor:copy-path': -> copyPathToClipboard(this, project, clipboard, false)
|
||||
'editor:copy-project-path': -> copyPathToClipboard(this, project, clipboard, true)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# root of the syntax tree to a token including _all_ scope names for the entire
|
||||
# path.
|
||||
#
|
||||
# Methods that take a `ScopeDescriptor` will also accept an {Array} of {Strings}
|
||||
# Methods that take a `ScopeDescriptor` will also accept an {Array} of {String}
|
||||
# scope names e.g. `['.source.js']`.
|
||||
#
|
||||
# You can use `ScopeDescriptor`s to get language-specific config settings via
|
||||
@@ -39,11 +39,17 @@ class ScopeDescriptor
|
||||
getScopesArray: -> @scopes
|
||||
|
||||
getScopeChain: ->
|
||||
@scopes
|
||||
.map (scope) ->
|
||||
scope = ".#{scope}" unless scope[0] is '.'
|
||||
scope
|
||||
.join(' ')
|
||||
# For backward compatibility, prefix TextMate-style scope names with
|
||||
# leading dots (e.g. 'source.js' -> '.source.js').
|
||||
if @scopes[0]?.includes('.')
|
||||
result = ''
|
||||
for scope, i in @scopes
|
||||
result += ' ' if i > 0
|
||||
result += '.' if scope[0] isnt '.'
|
||||
result += scope
|
||||
result
|
||||
else
|
||||
@scopes.join(' ')
|
||||
|
||||
toString: ->
|
||||
@getScopeChain()
|
||||
|
||||
@@ -448,9 +448,19 @@ class Selection {
|
||||
if (options.autoIndent && textIsAutoIndentable && !NonWhitespaceRegExp.test(precedingText) && (remainingLines.length > 0)) {
|
||||
autoIndentFirstLine = true
|
||||
const firstLine = precedingText + firstInsertedLine
|
||||
desiredIndentLevel = this.editor.tokenizedBuffer.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine)
|
||||
indentAdjustment = desiredIndentLevel - this.editor.indentLevelForLine(firstLine)
|
||||
this.adjustIndent(remainingLines, indentAdjustment)
|
||||
const languageMode = this.editor.buffer.getLanguageMode()
|
||||
desiredIndentLevel = (
|
||||
languageMode.suggestedIndentForLineAtBufferRow &&
|
||||
languageMode.suggestedIndentForLineAtBufferRow(
|
||||
oldBufferRange.start.row,
|
||||
firstLine,
|
||||
this.editor.getTabLength()
|
||||
)
|
||||
)
|
||||
if (desiredIndentLevel != null) {
|
||||
indentAdjustment = desiredIndentLevel - this.editor.indentLevelForLine(firstLine)
|
||||
this.adjustIndent(remainingLines, indentAdjustment)
|
||||
}
|
||||
}
|
||||
|
||||
text = firstInsertedLine
|
||||
@@ -575,7 +585,8 @@ class Selection {
|
||||
// is empty unless the selection spans multiple lines in which case all lines
|
||||
// are removed.
|
||||
deleteLine () {
|
||||
if (this.isEmpty()) {
|
||||
const range = this.getBufferRange()
|
||||
if (range.isEmpty()) {
|
||||
const start = this.cursor.getScreenRow()
|
||||
const range = this.editor.bufferRowsForScreenRows(start, start + 1)
|
||||
if (range[1] > range[0]) {
|
||||
@@ -584,12 +595,12 @@ class Selection {
|
||||
this.editor.buffer.deleteRow(range[0])
|
||||
}
|
||||
} else {
|
||||
const range = this.getBufferRange()
|
||||
const start = range.start.row
|
||||
let end = range.end.row
|
||||
if (end !== this.editor.buffer.getLastRow() && range.end.column === 0) end--
|
||||
this.editor.buffer.deleteRows(start, end)
|
||||
}
|
||||
this.cursor.setBufferPosition({row: this.cursor.getBufferRow(), column: range.start.column})
|
||||
}
|
||||
|
||||
// Public: Joins the current line with the one below it. Lines will
|
||||
@@ -821,8 +832,12 @@ class Selection {
|
||||
if (clippedRange.isEmpty()) continue
|
||||
}
|
||||
|
||||
const selection = this.editor.addSelectionForScreenRange(clippedRange)
|
||||
selection.setGoalScreenRange(range)
|
||||
const containingSelections = this.editor.selectionsMarkerLayer.findMarkers({containsScreenRange: clippedRange})
|
||||
if (containingSelections.length === 0) {
|
||||
const selection = this.editor.addSelectionForScreenRange(clippedRange)
|
||||
selection.setGoalScreenRange(range)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -843,8 +858,12 @@ class Selection {
|
||||
if (clippedRange.isEmpty()) continue
|
||||
}
|
||||
|
||||
const selection = this.editor.addSelectionForScreenRange(clippedRange)
|
||||
selection.setGoalScreenRange(range)
|
||||
const containingSelections = this.editor.selectionsMarkerLayer.findMarkers({containsScreenRange: clippedRange})
|
||||
if (containingSelections.length === 0) {
|
||||
const selection = this.editor.addSelectionForScreenRange(clippedRange)
|
||||
selection.setGoalScreenRange(range)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
178
src/syntax-scope-map.js
Normal file
178
src/syntax-scope-map.js
Normal file
@@ -0,0 +1,178 @@
|
||||
const parser = require('postcss-selector-parser')
|
||||
|
||||
module.exports =
|
||||
class SyntaxScopeMap {
|
||||
constructor (scopeNamesBySelector) {
|
||||
this.namedScopeTable = {}
|
||||
this.anonymousScopeTable = {}
|
||||
for (let selector in scopeNamesBySelector) {
|
||||
this.addSelector(selector, scopeNamesBySelector[selector])
|
||||
}
|
||||
setTableDefaults(this.namedScopeTable)
|
||||
setTableDefaults(this.anonymousScopeTable)
|
||||
}
|
||||
|
||||
addSelector (selector, scopeName) {
|
||||
parser((parseResult) => {
|
||||
for (let selectorNode of parseResult.nodes) {
|
||||
let currentTable = null
|
||||
let currentIndexValue = null
|
||||
|
||||
for (let i = selectorNode.nodes.length - 1; i >= 0; i--) {
|
||||
const termNode = selectorNode.nodes[i]
|
||||
|
||||
switch (termNode.type) {
|
||||
case 'tag':
|
||||
if (!currentTable) currentTable = this.namedScopeTable
|
||||
if (!currentTable[termNode.value]) currentTable[termNode.value] = {}
|
||||
currentTable = currentTable[termNode.value]
|
||||
if (currentIndexValue != null) {
|
||||
if (!currentTable.indices) currentTable.indices = {}
|
||||
if (!currentTable.indices[currentIndexValue]) currentTable.indices[currentIndexValue] = {}
|
||||
currentTable = currentTable.indices[currentIndexValue]
|
||||
currentIndexValue = null
|
||||
}
|
||||
break
|
||||
|
||||
case 'string':
|
||||
if (!currentTable) currentTable = this.anonymousScopeTable
|
||||
const value = termNode.value.slice(1, -1)
|
||||
if (!currentTable[value]) currentTable[value] = {}
|
||||
currentTable = currentTable[value]
|
||||
if (currentIndexValue != null) {
|
||||
if (!currentTable.indices) currentTable.indices = {}
|
||||
if (!currentTable.indices[currentIndexValue]) currentTable.indices[currentIndexValue] = {}
|
||||
currentTable = currentTable.indices[currentIndexValue]
|
||||
currentIndexValue = null
|
||||
}
|
||||
break
|
||||
|
||||
case 'universal':
|
||||
if (currentTable) {
|
||||
if (!currentTable['*']) currentTable['*'] = {}
|
||||
currentTable = currentTable['*']
|
||||
} else {
|
||||
if (!this.namedScopeTable['*']) {
|
||||
this.namedScopeTable['*'] = this.anonymousScopeTable['*'] = {}
|
||||
}
|
||||
currentTable = this.namedScopeTable['*']
|
||||
}
|
||||
if (currentIndexValue != null) {
|
||||
if (!currentTable.indices) currentTable.indices = {}
|
||||
if (!currentTable.indices[currentIndexValue]) currentTable.indices[currentIndexValue] = {}
|
||||
currentTable = currentTable.indices[currentIndexValue]
|
||||
currentIndexValue = null
|
||||
}
|
||||
break
|
||||
|
||||
case 'combinator':
|
||||
if (currentIndexValue != null) {
|
||||
rejectSelector(selector)
|
||||
}
|
||||
|
||||
if (termNode.value === '>') {
|
||||
if (!currentTable.parents) currentTable.parents = {}
|
||||
currentTable = currentTable.parents
|
||||
} else {
|
||||
rejectSelector(selector)
|
||||
}
|
||||
break
|
||||
|
||||
case 'pseudo':
|
||||
if (termNode.value === ':nth-child') {
|
||||
currentIndexValue = termNode.nodes[0].nodes[0].value
|
||||
} else {
|
||||
rejectSelector(selector)
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
rejectSelector(selector)
|
||||
}
|
||||
}
|
||||
|
||||
currentTable.scopeName = scopeName
|
||||
}
|
||||
}).process(selector)
|
||||
}
|
||||
|
||||
get (nodeTypes, childIndices, leafIsNamed = true) {
|
||||
let result
|
||||
let i = nodeTypes.length - 1
|
||||
let currentTable = leafIsNamed
|
||||
? this.namedScopeTable[nodeTypes[i]]
|
||||
: this.anonymousScopeTable[nodeTypes[i]]
|
||||
|
||||
if (!currentTable) currentTable = this.namedScopeTable['*']
|
||||
|
||||
while (currentTable) {
|
||||
if (currentTable.indices && currentTable.indices[childIndices[i]]) {
|
||||
currentTable = currentTable.indices[childIndices[i]]
|
||||
}
|
||||
|
||||
if (currentTable.scopeName) {
|
||||
result = currentTable.scopeName
|
||||
}
|
||||
|
||||
if (i === 0) break
|
||||
i--
|
||||
currentTable = currentTable.parents && (
|
||||
currentTable.parents[nodeTypes[i]] ||
|
||||
currentTable.parents['*']
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
function setTableDefaults (table) {
|
||||
const defaultTypeTable = table['*']
|
||||
|
||||
for (let type in table) {
|
||||
let typeTable = table[type]
|
||||
if (typeTable === defaultTypeTable) continue
|
||||
|
||||
if (defaultTypeTable) {
|
||||
mergeTable(typeTable, defaultTypeTable)
|
||||
}
|
||||
|
||||
if (typeTable.parents) {
|
||||
setTableDefaults(typeTable.parents)
|
||||
}
|
||||
|
||||
for (let key in typeTable.indices) {
|
||||
const indexTable = typeTable.indices[key]
|
||||
mergeTable(indexTable, typeTable, false)
|
||||
if (indexTable.parents) {
|
||||
setTableDefaults(indexTable.parents)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mergeTable (table, defaultTable, mergeIndices = true) {
|
||||
if (mergeIndices && defaultTable.indices) {
|
||||
if (!table.indices) table.indices = {}
|
||||
for (let key in defaultTable.indices) {
|
||||
if (!table.indices[key]) table.indices[key] = {}
|
||||
mergeTable(table.indices[key], defaultTable.indices[key])
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultTable.parents) {
|
||||
if (!table.parents) table.parents = {}
|
||||
for (let key in defaultTable.parents) {
|
||||
if (!table.parents[key]) table.parents[key] = {}
|
||||
mergeTable(table.parents[key], defaultTable.parents[key])
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultTable.scopeName && !table.scopeName) {
|
||||
table.scopeName = defaultTable.scopeName
|
||||
}
|
||||
}
|
||||
|
||||
function rejectSelector (selector) {
|
||||
throw new TypeError(`Unsupported selector '${selector}'`)
|
||||
}
|
||||
@@ -56,7 +56,7 @@ class TextEditorComponent {
|
||||
this.props = props
|
||||
|
||||
if (!props.model) {
|
||||
props.model = new TextEditor({mini: props.mini})
|
||||
props.model = new TextEditor({mini: props.mini, readOnly: props.readOnly})
|
||||
}
|
||||
this.props.model.component = this
|
||||
|
||||
@@ -170,6 +170,7 @@ class TextEditorComponent {
|
||||
this.textDecorationBoundaries = []
|
||||
this.pendingScrollTopRow = this.props.initialScrollTopRow
|
||||
this.pendingScrollLeftColumn = this.props.initialScrollLeftColumn
|
||||
this.tabIndex = this.props.element && this.props.element.tabIndex ? this.props.element.tabIndex : -1
|
||||
|
||||
this.measuredContent = false
|
||||
this.queryGuttersToRender()
|
||||
@@ -467,9 +468,13 @@ class TextEditorComponent {
|
||||
}
|
||||
}
|
||||
|
||||
let attributes = null
|
||||
let attributes = {}
|
||||
if (model.isMini()) {
|
||||
attributes = {mini: ''}
|
||||
attributes.mini = ''
|
||||
}
|
||||
|
||||
if (!this.isInputEnabled()) {
|
||||
attributes.readonly = ''
|
||||
}
|
||||
|
||||
const dataset = {encoding: model.getEncoding()}
|
||||
@@ -579,7 +584,6 @@ class TextEditorComponent {
|
||||
on: {mousedown: this.didMouseDownOnContent},
|
||||
style
|
||||
},
|
||||
this.renderHighlightDecorations(),
|
||||
this.renderLineTiles(),
|
||||
this.renderBlockDecorationMeasurementArea(),
|
||||
this.renderCharacterMeasurementLine()
|
||||
@@ -597,13 +601,15 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
renderLineTiles () {
|
||||
const children = []
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
contain: 'strict',
|
||||
overflow: 'hidden'
|
||||
}
|
||||
|
||||
const children = []
|
||||
children.push(this.renderHighlightDecorations())
|
||||
|
||||
if (this.hasInitialMeasurements) {
|
||||
const {lineComponentsByScreenLineId} = this
|
||||
|
||||
@@ -684,7 +690,8 @@ class TextEditorComponent {
|
||||
scrollWidth: this.getScrollWidth(),
|
||||
decorationsToRender: this.decorationsToRender,
|
||||
cursorsBlinkedOff: this.cursorsBlinkedOff,
|
||||
hiddenInputPosition: this.hiddenInputPosition
|
||||
hiddenInputPosition: this.hiddenInputPosition,
|
||||
tabIndex: this.tabIndex
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1517,28 +1524,28 @@ class TextEditorComponent {
|
||||
didMouseWheel (event) {
|
||||
const scrollSensitivity = this.props.model.getScrollSensitivity() / 100
|
||||
|
||||
let {deltaX, deltaY} = event
|
||||
let {wheelDeltaX, wheelDeltaY} = event
|
||||
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
||||
deltaX = (Math.sign(deltaX) === 1)
|
||||
? Math.max(1, deltaX * scrollSensitivity)
|
||||
: Math.min(-1, deltaX * scrollSensitivity)
|
||||
deltaY = 0
|
||||
if (Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY)) {
|
||||
wheelDeltaX = (Math.sign(wheelDeltaX) === 1)
|
||||
? Math.max(1, wheelDeltaX * scrollSensitivity)
|
||||
: Math.min(-1, wheelDeltaX * scrollSensitivity)
|
||||
wheelDeltaY = 0
|
||||
} else {
|
||||
deltaX = 0
|
||||
deltaY = (Math.sign(deltaY) === 1)
|
||||
? Math.max(1, deltaY * scrollSensitivity)
|
||||
: Math.min(-1, deltaY * scrollSensitivity)
|
||||
wheelDeltaX = 0
|
||||
wheelDeltaY = (Math.sign(wheelDeltaY) === 1)
|
||||
? Math.max(1, wheelDeltaY * scrollSensitivity)
|
||||
: Math.min(-1, wheelDeltaY * scrollSensitivity)
|
||||
}
|
||||
|
||||
if (this.getPlatform() !== 'darwin' && event.shiftKey) {
|
||||
let temp = deltaX
|
||||
deltaX = deltaY
|
||||
deltaY = temp
|
||||
let temp = wheelDeltaX
|
||||
wheelDeltaX = wheelDeltaY
|
||||
wheelDeltaY = temp
|
||||
}
|
||||
|
||||
const scrollLeftChanged = deltaX !== 0 && this.setScrollLeft(this.getScrollLeft() + deltaX)
|
||||
const scrollTopChanged = deltaY !== 0 && this.setScrollTop(this.getScrollTop() + deltaY)
|
||||
const scrollLeftChanged = wheelDeltaX !== 0 && this.setScrollLeft(this.getScrollLeft() - wheelDeltaX)
|
||||
const scrollTopChanged = wheelDeltaY !== 0 && this.setScrollTop(this.getScrollTop() - wheelDeltaY)
|
||||
|
||||
if (scrollLeftChanged || scrollTopChanged) this.updateSync()
|
||||
}
|
||||
@@ -1719,10 +1726,6 @@ class TextEditorComponent {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.getChromeVersion() === 56) {
|
||||
this.getHiddenInput().value = ''
|
||||
}
|
||||
|
||||
this.compositionCheckpoint = this.props.model.createCheckpoint()
|
||||
if (this.accentedCharacterMenuIsOpen) {
|
||||
this.props.model.selectLeft()
|
||||
@@ -1730,16 +1733,7 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
didCompositionUpdate (event) {
|
||||
if (this.getChromeVersion() === 56) {
|
||||
process.nextTick(() => {
|
||||
if (this.compositionCheckpoint != null) {
|
||||
const previewText = this.getHiddenInput().value
|
||||
this.props.model.insertText(previewText, {select: true})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.props.model.insertText(event.data, {select: true})
|
||||
}
|
||||
this.props.model.insertText(event.data, {select: true})
|
||||
}
|
||||
|
||||
didCompositionEnd (event) {
|
||||
@@ -1763,33 +1757,30 @@ class TextEditorComponent {
|
||||
}
|
||||
}
|
||||
|
||||
// On Linux, position the cursor on middle mouse button click. A
|
||||
// textInput event with the contents of the selection clipboard will be
|
||||
// dispatched by the browser automatically on mouseup.
|
||||
if (platform === 'linux' && button === 1) {
|
||||
const selection = clipboard.readText('selection')
|
||||
const screenPosition = this.screenPositionForMouseEvent(event)
|
||||
model.setCursorScreenPosition(screenPosition, {autoscroll: false})
|
||||
model.insertText(selection)
|
||||
const screenPosition = this.screenPositionForMouseEvent(event)
|
||||
|
||||
if (button !== 0 || (platform === 'darwin' && ctrlKey)) {
|
||||
// Always set cursor position on middle-click
|
||||
// Only set cursor position on right-click if there is one cursor with no selection
|
||||
const ranges = model.getSelectedBufferRanges()
|
||||
if (button === 1 || (ranges.length === 1 && ranges[0].isEmpty())) {
|
||||
model.setCursorScreenPosition(screenPosition, {autoscroll: false})
|
||||
}
|
||||
|
||||
// On Linux, pasting happens on middle click. A textInput event with the
|
||||
// contents of the selection clipboard will be dispatched by the browser
|
||||
// automatically on mouseup.
|
||||
if (platform === 'linux' && button === 1) model.insertText(clipboard.readText('selection'))
|
||||
return
|
||||
}
|
||||
|
||||
// Only handle mousedown events for left mouse button (or the middle mouse
|
||||
// button on Linux where it pastes the selection clipboard).
|
||||
if (button !== 0) return
|
||||
|
||||
// Ctrl-click brings up the context menu on macOS
|
||||
if (platform === 'darwin' && ctrlKey) return
|
||||
|
||||
const screenPosition = this.screenPositionForMouseEvent(event)
|
||||
|
||||
if (target && target.matches('.fold-marker')) {
|
||||
const bufferPosition = model.bufferPositionForScreenPosition(screenPosition)
|
||||
model.destroyFoldsContainingBufferPositions([bufferPosition], false)
|
||||
return
|
||||
}
|
||||
|
||||
const addOrRemoveSelection = metaKey || (ctrlKey && platform !== 'darwin')
|
||||
const addOrRemoveSelection = metaKey || ctrlKey
|
||||
|
||||
switch (detail) {
|
||||
case 1:
|
||||
@@ -2974,11 +2965,11 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
setInputEnabled (inputEnabled) {
|
||||
this.props.inputEnabled = inputEnabled
|
||||
this.props.model.update({readOnly: !inputEnabled})
|
||||
}
|
||||
|
||||
isInputEnabled (inputEnabled) {
|
||||
return this.props.inputEnabled != null ? this.props.inputEnabled : true
|
||||
return !this.props.model.isReadOnly()
|
||||
}
|
||||
|
||||
getHiddenInput () {
|
||||
@@ -3029,7 +3020,7 @@ class DummyScrollbarComponent {
|
||||
|
||||
const outerStyle = {
|
||||
position: 'absolute',
|
||||
contain: 'strict',
|
||||
contain: 'content',
|
||||
zIndex: 1,
|
||||
willChange: 'transform'
|
||||
}
|
||||
@@ -3552,7 +3543,8 @@ class CursorsAndInputComponent {
|
||||
zIndex: 1,
|
||||
width: scrollWidth + 'px',
|
||||
height: scrollHeight + 'px',
|
||||
pointerEvents: 'none'
|
||||
pointerEvents: 'none',
|
||||
userSelect: 'none'
|
||||
}
|
||||
}, children)
|
||||
}
|
||||
@@ -3565,7 +3557,7 @@ class CursorsAndInputComponent {
|
||||
const {
|
||||
lineHeight, hiddenInputPosition, didBlurHiddenInput, didFocusHiddenInput,
|
||||
didPaste, didTextInput, didKeydown, didKeyup, didKeypress,
|
||||
didCompositionStart, didCompositionUpdate, didCompositionEnd
|
||||
didCompositionStart, didCompositionUpdate, didCompositionEnd, tabIndex
|
||||
} = this.props
|
||||
|
||||
let top, left
|
||||
@@ -3593,7 +3585,7 @@ class CursorsAndInputComponent {
|
||||
compositionupdate: didCompositionUpdate,
|
||||
compositionend: didCompositionEnd
|
||||
},
|
||||
tabIndex: -1,
|
||||
tabIndex: tabIndex,
|
||||
style: {
|
||||
position: 'absolute',
|
||||
width: '1px',
|
||||
@@ -4028,6 +4020,7 @@ class HighlightsComponent {
|
||||
this.element.style.contain = 'strict'
|
||||
this.element.style.position = 'absolute'
|
||||
this.element.style.overflow = 'hidden'
|
||||
this.element.style.userSelect = 'none'
|
||||
this.highlightComponentsByKey = new Map()
|
||||
this.update(props)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class TextEditorElement extends HTMLElement {
|
||||
createdCallback () {
|
||||
this.emitter = new Emitter()
|
||||
this.initialText = this.textContent
|
||||
this.tabIndex = -1
|
||||
if (this.tabIndex == null) this.tabIndex = -1
|
||||
this.addEventListener('focus', (event) => this.getComponent().didFocus(event))
|
||||
this.addEventListener('blur', (event) => this.getComponent().didBlur(event))
|
||||
}
|
||||
@@ -59,6 +59,9 @@ class TextEditorElement extends HTMLElement {
|
||||
case 'gutter-hidden':
|
||||
this.getModel().update({lineNumberGutterVisible: newValue == null})
|
||||
break
|
||||
case 'readonly':
|
||||
this.getModel().update({readOnly: newValue != null})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,7 +278,8 @@ class TextEditorElement extends HTMLElement {
|
||||
this.component = new TextEditorComponent({
|
||||
element: this,
|
||||
mini: this.hasAttribute('mini'),
|
||||
updatedSynchronously: this.updatedSynchronously
|
||||
updatedSynchronously: this.updatedSynchronously,
|
||||
readOnly: this.hasAttribute('readonly')
|
||||
})
|
||||
this.updateModelFromAttributes()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
/** @babel */
|
||||
|
||||
import {Emitter, Disposable, CompositeDisposable} from 'event-kit'
|
||||
import {Point, Range} from 'text-buffer'
|
||||
import TextEditor from './text-editor'
|
||||
import ScopeDescriptor from './scope-descriptor'
|
||||
const _ = require('underscore-plus')
|
||||
const {Emitter, Disposable, CompositeDisposable} = require('event-kit')
|
||||
const TextEditor = require('./text-editor')
|
||||
const ScopeDescriptor = require('./scope-descriptor')
|
||||
|
||||
const EDITOR_PARAMS_BY_SETTING_KEY = [
|
||||
['core.fileEncoding', 'encoding'],
|
||||
@@ -23,12 +21,9 @@ const EDITOR_PARAMS_BY_SETTING_KEY = [
|
||||
['editor.autoIndentOnPaste', 'autoIndentOnPaste'],
|
||||
['editor.scrollPastEnd', 'scrollPastEnd'],
|
||||
['editor.undoGroupingInterval', 'undoGroupingInterval'],
|
||||
['editor.nonWordCharacters', 'nonWordCharacters'],
|
||||
['editor.scrollSensitivity', 'scrollSensitivity']
|
||||
]
|
||||
|
||||
const GRAMMAR_SELECTION_RANGE = Range(Point.ZERO, Point(10, 0)).freeze()
|
||||
|
||||
// Experimental: This global registry tracks registered `TextEditors`.
|
||||
//
|
||||
// If you want to add functionality to a wider set of text editors than just
|
||||
@@ -40,13 +35,11 @@ const GRAMMAR_SELECTION_RANGE = Range(Point.ZERO, Point(10, 0)).freeze()
|
||||
// them for observation via `atom.textEditors.add`. **Important:** When you're
|
||||
// done using your editor, be sure to call `dispose` on the returned disposable
|
||||
// to avoid leaking editors.
|
||||
export default class TextEditorRegistry {
|
||||
constructor ({config, grammarRegistry, assert, packageManager}) {
|
||||
module.exports =
|
||||
class TextEditorRegistry {
|
||||
constructor ({config, assert, packageManager}) {
|
||||
this.assert = assert
|
||||
this.config = config
|
||||
this.grammarRegistry = grammarRegistry
|
||||
this.scopedSettingsDelegate = new ScopedSettingsDelegate(config)
|
||||
this.grammarAddedOrUpdated = this.grammarAddedOrUpdated.bind(this)
|
||||
this.clear()
|
||||
|
||||
this.initialPackageActivationPromise = new Promise((resolve) => {
|
||||
@@ -83,10 +76,6 @@ export default class TextEditorRegistry {
|
||||
this.editorsWithMaintainedGrammar = new Set()
|
||||
this.editorGrammarOverrides = {}
|
||||
this.editorGrammarScores = new WeakMap()
|
||||
this.subscriptions.add(
|
||||
this.grammarRegistry.onDidAddGrammar(this.grammarAddedOrUpdated),
|
||||
this.grammarRegistry.onDidUpdateGrammar(this.grammarAddedOrUpdated)
|
||||
)
|
||||
}
|
||||
|
||||
destroy () {
|
||||
@@ -114,10 +103,10 @@ export default class TextEditorRegistry {
|
||||
|
||||
let scope = null
|
||||
if (params.buffer) {
|
||||
const filePath = params.buffer.getPath()
|
||||
const headContent = params.buffer.getTextInRange(GRAMMAR_SELECTION_RANGE)
|
||||
params.grammar = this.grammarRegistry.selectGrammar(filePath, headContent)
|
||||
scope = new ScopeDescriptor({scopes: [params.grammar.scopeName]})
|
||||
const {grammar} = params.buffer.getLanguageMode()
|
||||
if (grammar) {
|
||||
scope = new ScopeDescriptor({scopes: [grammar.scopeName]})
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(params, this.textEditorParamsForScope(scope))
|
||||
@@ -159,13 +148,11 @@ export default class TextEditorRegistry {
|
||||
}
|
||||
this.editorsWithMaintainedConfig.add(editor)
|
||||
|
||||
editor.setScopedSettingsDelegate(this.scopedSettingsDelegate)
|
||||
|
||||
this.subscribeToSettingsForEditorScope(editor)
|
||||
const grammarChangeSubscription = editor.onDidChangeGrammar(() => {
|
||||
this.subscribeToSettingsForEditorScope(editor)
|
||||
this.updateAndMonitorEditorSettings(editor)
|
||||
const languageChangeSubscription = editor.buffer.onDidChangeLanguageMode((newLanguageMode, oldLanguageMode) => {
|
||||
this.updateAndMonitorEditorSettings(editor, oldLanguageMode)
|
||||
})
|
||||
this.subscriptions.add(grammarChangeSubscription)
|
||||
this.subscriptions.add(languageChangeSubscription)
|
||||
|
||||
const updateTabTypes = () => {
|
||||
const configOptions = {scope: editor.getRootScopeDescriptor()}
|
||||
@@ -182,152 +169,87 @@ export default class TextEditorRegistry {
|
||||
|
||||
return new Disposable(() => {
|
||||
this.editorsWithMaintainedConfig.delete(editor)
|
||||
editor.setScopedSettingsDelegate(null)
|
||||
tokenizeSubscription.dispose()
|
||||
grammarChangeSubscription.dispose()
|
||||
this.subscriptions.remove(grammarChangeSubscription)
|
||||
languageChangeSubscription.dispose()
|
||||
this.subscriptions.remove(languageChangeSubscription)
|
||||
this.subscriptions.remove(tokenizeSubscription)
|
||||
})
|
||||
}
|
||||
|
||||
// Set a {TextEditor}'s grammar based on its path and content, and continue
|
||||
// to update its grammar as grammars are added or updated, or the editor's
|
||||
// file path changes.
|
||||
// Deprecated: set a {TextEditor}'s grammar based on its path and content,
|
||||
// and continue to update its grammar as grammars are added or updated, or
|
||||
// the editor's file path changes.
|
||||
//
|
||||
// * `editor` The editor whose grammar will be maintained.
|
||||
//
|
||||
// Returns a {Disposable} that can be used to stop updating the editor's
|
||||
// grammar.
|
||||
maintainGrammar (editor) {
|
||||
if (this.editorsWithMaintainedGrammar.has(editor)) {
|
||||
return new Disposable(noop)
|
||||
}
|
||||
|
||||
this.editorsWithMaintainedGrammar.add(editor)
|
||||
|
||||
const buffer = editor.getBuffer()
|
||||
for (let existingEditor of this.editorsWithMaintainedGrammar) {
|
||||
if (existingEditor.getBuffer() === buffer) {
|
||||
const existingOverride = this.editorGrammarOverrides[existingEditor.id]
|
||||
if (existingOverride) {
|
||||
this.editorGrammarOverrides[editor.id] = existingOverride
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
this.selectGrammarForEditor(editor)
|
||||
|
||||
const pathChangeSubscription = editor.onDidChangePath(() => {
|
||||
this.editorGrammarScores.delete(editor)
|
||||
this.selectGrammarForEditor(editor)
|
||||
})
|
||||
|
||||
this.subscriptions.add(pathChangeSubscription)
|
||||
|
||||
return new Disposable(() => {
|
||||
delete this.editorGrammarOverrides[editor.id]
|
||||
this.editorsWithMaintainedGrammar.delete(editor)
|
||||
this.subscriptions.remove(pathChangeSubscription)
|
||||
pathChangeSubscription.dispose()
|
||||
})
|
||||
atom.grammars.maintainGrammar(editor.getBuffer())
|
||||
}
|
||||
|
||||
// Force a {TextEditor} to use a different grammar than the one that would
|
||||
// otherwise be selected for it.
|
||||
// Deprecated: Force a {TextEditor} to use a different grammar than the
|
||||
// one that would otherwise be selected for it.
|
||||
//
|
||||
// * `editor` The editor whose gramamr will be set.
|
||||
// * `scopeName` The {String} root scope name for the desired {Grammar}.
|
||||
setGrammarOverride (editor, scopeName) {
|
||||
this.editorGrammarOverrides[editor.id] = scopeName
|
||||
this.editorGrammarScores.delete(editor)
|
||||
editor.setGrammar(this.grammarRegistry.grammarForScopeName(scopeName))
|
||||
// * `languageId` The {String} language ID for the desired {Grammar}.
|
||||
setGrammarOverride (editor, languageId) {
|
||||
atom.grammars.assignLanguageMode(editor.getBuffer(), languageId)
|
||||
}
|
||||
|
||||
// Retrieve the grammar scope name that has been set as a grammar override
|
||||
// for the given {TextEditor}.
|
||||
// Deprecated: Retrieve the grammar scope name that has been set as a
|
||||
// grammar override for the given {TextEditor}.
|
||||
//
|
||||
// * `editor` The editor.
|
||||
//
|
||||
// Returns a {String} scope name, or `null` if no override has been set
|
||||
// for the given editor.
|
||||
getGrammarOverride (editor) {
|
||||
return this.editorGrammarOverrides[editor.id]
|
||||
return editor.getBuffer().getLanguageMode().grammar.scopeName
|
||||
}
|
||||
|
||||
// Remove any grammar override that has been set for the given {TextEditor}.
|
||||
// Deprecated: Remove any grammar override that has been set for the given {TextEditor}.
|
||||
//
|
||||
// * `editor` The editor.
|
||||
clearGrammarOverride (editor) {
|
||||
delete this.editorGrammarOverrides[editor.id]
|
||||
this.selectGrammarForEditor(editor)
|
||||
atom.grammars.autoAssignLanguageMode(editor.getBuffer())
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
grammarAddedOrUpdated (grammar) {
|
||||
this.editorsWithMaintainedGrammar.forEach((editor) => {
|
||||
if (grammar.injectionSelector) {
|
||||
if (editor.tokenizedBuffer.hasTokenForSelector(grammar.injectionSelector)) {
|
||||
editor.tokenizedBuffer.retokenizeLines()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const grammarOverride = this.editorGrammarOverrides[editor.id]
|
||||
if (grammarOverride) {
|
||||
if (grammar.scopeName === grammarOverride) {
|
||||
editor.setGrammar(grammar)
|
||||
}
|
||||
} else {
|
||||
const score = this.grammarRegistry.getGrammarScore(
|
||||
grammar,
|
||||
editor.getPath(),
|
||||
editor.getTextInBufferRange(GRAMMAR_SELECTION_RANGE)
|
||||
)
|
||||
|
||||
let currentScore = this.editorGrammarScores.get(editor)
|
||||
if (currentScore == null || score > currentScore) {
|
||||
editor.setGrammar(grammar)
|
||||
this.editorGrammarScores.set(editor, score)
|
||||
}
|
||||
}
|
||||
})
|
||||
async updateAndMonitorEditorSettings (editor, oldLanguageMode) {
|
||||
await this.initialPackageActivationPromise
|
||||
this.updateEditorSettingsForLanguageMode(editor, oldLanguageMode)
|
||||
await this.subscribeToSettingsForEditorScope(editor)
|
||||
}
|
||||
|
||||
selectGrammarForEditor (editor) {
|
||||
const grammarOverride = this.editorGrammarOverrides[editor.id]
|
||||
updateEditorSettingsForLanguageMode (editor, oldLanguageMode) {
|
||||
const newLanguageMode = editor.buffer.getLanguageMode()
|
||||
|
||||
if (grammarOverride) {
|
||||
const grammar = this.grammarRegistry.grammarForScopeName(grammarOverride)
|
||||
editor.setGrammar(grammar)
|
||||
return
|
||||
}
|
||||
if (oldLanguageMode) {
|
||||
const newSettings = this.textEditorParamsForScope(newLanguageMode.rootScopeDescriptor)
|
||||
const oldSettings = this.textEditorParamsForScope(oldLanguageMode.rootScopeDescriptor)
|
||||
|
||||
const {grammar, score} = this.grammarRegistry.selectGrammarWithScore(
|
||||
editor.getPath(),
|
||||
editor.getTextInBufferRange(GRAMMAR_SELECTION_RANGE)
|
||||
)
|
||||
const updatedSettings = {}
|
||||
for (const [, paramName] of EDITOR_PARAMS_BY_SETTING_KEY) {
|
||||
// Update the setting only if it has changed between the two language
|
||||
// modes. This prevents user-modified settings in an editor (like
|
||||
// 'softWrapped') from being reset when the language mode changes.
|
||||
if (!_.isEqual(newSettings[paramName], oldSettings[paramName])) {
|
||||
updatedSettings[paramName] = newSettings[paramName]
|
||||
}
|
||||
}
|
||||
|
||||
if (!grammar) {
|
||||
throw new Error(`No grammar found for path: ${editor.getPath()}`)
|
||||
}
|
||||
|
||||
const currentScore = this.editorGrammarScores.get(editor)
|
||||
if (currentScore == null || score > currentScore) {
|
||||
editor.setGrammar(grammar)
|
||||
this.editorGrammarScores.set(editor, score)
|
||||
if (_.size(updatedSettings) > 0) {
|
||||
editor.update(updatedSettings)
|
||||
}
|
||||
} else {
|
||||
editor.update(this.textEditorParamsForScope(newLanguageMode.rootScopeDescriptor))
|
||||
}
|
||||
}
|
||||
|
||||
async subscribeToSettingsForEditorScope (editor) {
|
||||
await this.initialPackageActivationPromise
|
||||
|
||||
const scopeDescriptor = editor.getRootScopeDescriptor()
|
||||
const scopeChain = scopeDescriptor.getScopeChain()
|
||||
|
||||
editor.update(this.textEditorParamsForScope(scopeDescriptor))
|
||||
|
||||
if (!this.scopesWithConfigSubscriptions.has(scopeChain)) {
|
||||
this.scopesWithConfigSubscriptions.add(scopeChain)
|
||||
const configOptions = {scope: scopeDescriptor}
|
||||
@@ -390,44 +312,3 @@ function shouldEditorUseSoftTabs (editor, tabType, softTabs) {
|
||||
}
|
||||
|
||||
function noop () {}
|
||||
|
||||
class ScopedSettingsDelegate {
|
||||
constructor (config) {
|
||||
this.config = config
|
||||
}
|
||||
|
||||
getNonWordCharacters (scope) {
|
||||
return this.config.get('editor.nonWordCharacters', {scope: scope})
|
||||
}
|
||||
|
||||
getIncreaseIndentPattern (scope) {
|
||||
return this.config.get('editor.increaseIndentPattern', {scope: scope})
|
||||
}
|
||||
|
||||
getDecreaseIndentPattern (scope) {
|
||||
return this.config.get('editor.decreaseIndentPattern', {scope: scope})
|
||||
}
|
||||
|
||||
getDecreaseNextIndentPattern (scope) {
|
||||
return this.config.get('editor.decreaseNextIndentPattern', {scope: scope})
|
||||
}
|
||||
|
||||
getFoldEndPattern (scope) {
|
||||
return this.config.get('editor.foldEndPattern', {scope: scope})
|
||||
}
|
||||
|
||||
getCommentStrings (scope) {
|
||||
const commentStartEntries = this.config.getAll('editor.commentStart', {scope})
|
||||
const commentEndEntries = this.config.getAll('editor.commentEnd', {scope})
|
||||
const commentStartEntry = commentStartEntries[0]
|
||||
const commentEndEntry = commentEndEntries.find((entry) => {
|
||||
return entry.scopeSelector === commentStartEntry.scopeSelector
|
||||
})
|
||||
return {
|
||||
commentStartString: commentStartEntry && commentStartEntry.value,
|
||||
commentEndString: commentEndEntry && commentEndEntry.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextEditorRegistry.ScopedSettingsDelegate = ScopedSettingsDelegate
|
||||
|
||||
@@ -7,9 +7,10 @@ const {CompositeDisposable, Disposable, Emitter} = require('event-kit')
|
||||
const TextBuffer = require('text-buffer')
|
||||
const {Point, Range} = TextBuffer
|
||||
const DecorationManager = require('./decoration-manager')
|
||||
const TokenizedBuffer = require('./tokenized-buffer')
|
||||
const Cursor = require('./cursor')
|
||||
const Selection = require('./selection')
|
||||
const NullGrammar = require('./null-grammar')
|
||||
const TextMateLanguageMode = require('./text-mate-language-mode')
|
||||
|
||||
const TextMateScopeSelector = require('first-mate').ScopeSelector
|
||||
const GutterContainer = require('./gutter-container')
|
||||
@@ -22,6 +23,8 @@ const NON_WHITESPACE_REGEXP = /\S/
|
||||
const ZERO_WIDTH_NBSP = '\ufeff'
|
||||
let nextId = 0
|
||||
|
||||
const DEFAULT_NON_WORD_CHARACTERS = "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-…"
|
||||
|
||||
// Essential: This class represents all essential editing state for a single
|
||||
// {TextBuffer}, including cursor and selection positions, folds, and soft wraps.
|
||||
// If you're manipulating the state of an editor, use this class.
|
||||
@@ -86,12 +89,13 @@ class TextEditor {
|
||||
static deserialize (state, atomEnvironment) {
|
||||
if (state.version !== SERIALIZATION_VERSION) return null
|
||||
|
||||
try {
|
||||
const tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment)
|
||||
if (!tokenizedBuffer) return null
|
||||
let bufferId = state.tokenizedBuffer
|
||||
? state.tokenizedBuffer.bufferId
|
||||
: state.bufferId
|
||||
|
||||
state.tokenizedBuffer = tokenizedBuffer
|
||||
state.tabLength = state.tokenizedBuffer.getTabLength()
|
||||
try {
|
||||
state.buffer = atomEnvironment.project.bufferForIdSync(bufferId)
|
||||
if (!state.buffer) return null
|
||||
} catch (error) {
|
||||
if (error.syscall === 'read') {
|
||||
return // Error reading the file, don't deserialize an editor for it
|
||||
@@ -100,7 +104,6 @@ class TextEditor {
|
||||
}
|
||||
}
|
||||
|
||||
state.buffer = state.tokenizedBuffer.buffer
|
||||
state.assert = atomEnvironment.assert.bind(atomEnvironment)
|
||||
const editor = new TextEditor(state)
|
||||
if (state.registered) {
|
||||
@@ -116,14 +119,18 @@ class TextEditor {
|
||||
}
|
||||
|
||||
this.id = params.id != null ? params.id : nextId++
|
||||
if (this.id >= nextId) {
|
||||
// Ensure that new editors get unique ids:
|
||||
nextId = this.id + 1
|
||||
}
|
||||
this.initialScrollTopRow = params.initialScrollTopRow
|
||||
this.initialScrollLeftColumn = params.initialScrollLeftColumn
|
||||
this.decorationManager = params.decorationManager
|
||||
this.selectionsMarkerLayer = params.selectionsMarkerLayer
|
||||
this.mini = (params.mini != null) ? params.mini : false
|
||||
this.readOnly = (params.readOnly != null) ? params.readOnly : false
|
||||
this.placeholderText = params.placeholderText
|
||||
this.showLineNumbers = params.showLineNumbers
|
||||
this.largeFileMode = params.largeFileMode
|
||||
this.assert = params.assert || (condition => condition)
|
||||
this.showInvisibles = (params.showInvisibles != null) ? params.showInvisibles : true
|
||||
this.autoHeight = params.autoHeight
|
||||
@@ -142,7 +149,6 @@ class TextEditor {
|
||||
this.autoIndent = (params.autoIndent != null) ? params.autoIndent : true
|
||||
this.autoIndentOnPaste = (params.autoIndentOnPaste != null) ? params.autoIndentOnPaste : true
|
||||
this.undoGroupingInterval = (params.undoGroupingInterval != null) ? params.undoGroupingInterval : 300
|
||||
this.nonWordCharacters = (params.nonWordCharacters != null) ? params.nonWordCharacters : "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-…"
|
||||
this.softWrapped = (params.softWrapped != null) ? params.softWrapped : false
|
||||
this.softWrapAtPreferredLineLength = (params.softWrapAtPreferredLineLength != null) ? params.softWrapAtPreferredLineLength : false
|
||||
this.preferredLineLength = (params.preferredLineLength != null) ? params.preferredLineLength : 80
|
||||
@@ -171,17 +177,20 @@ class TextEditor {
|
||||
this.selections = []
|
||||
this.hasTerminatedPendingState = false
|
||||
|
||||
this.buffer = params.buffer || new TextBuffer({
|
||||
shouldDestroyOnFileDelete () { return atom.config.get('core.closeDeletedFileTabs') }
|
||||
})
|
||||
if (params.buffer) {
|
||||
this.buffer = params.buffer
|
||||
} else {
|
||||
this.buffer = new TextBuffer({
|
||||
shouldDestroyOnFileDelete () { return atom.config.get('core.closeDeletedFileTabs') }
|
||||
})
|
||||
this.buffer.setLanguageMode(new TextMateLanguageMode({buffer: this.buffer, config: atom.config}))
|
||||
}
|
||||
|
||||
this.tokenizedBuffer = params.tokenizedBuffer || new TokenizedBuffer({
|
||||
grammar: params.grammar,
|
||||
tabLength,
|
||||
buffer: this.buffer,
|
||||
largeFileMode: this.largeFileMode,
|
||||
assert: this.assert
|
||||
const languageMode = this.buffer.getLanguageMode()
|
||||
this.languageModeSubscription = languageMode.onDidTokenize && languageMode.onDidTokenize(() => {
|
||||
this.emitter.emit('did-tokenize')
|
||||
})
|
||||
if (this.languageModeSubscription) this.disposables.add(this.languageModeSubscription)
|
||||
|
||||
if (params.displayLayer) {
|
||||
this.displayLayer = params.displayLayer
|
||||
@@ -217,8 +226,6 @@ class TextEditor {
|
||||
this.selectionsMarkerLayer = this.addMarkerLayer({maintainHistory: true, persistent: true})
|
||||
}
|
||||
|
||||
this.displayLayer.setTextDecorationLayer(this.tokenizedBuffer)
|
||||
|
||||
this.decorationManager = new DecorationManager(this)
|
||||
this.decorateMarkerLayer(this.selectionsMarkerLayer, {type: 'cursor'})
|
||||
if (!this.isMini()) this.decorateCursorLine()
|
||||
@@ -271,9 +278,8 @@ class TextEditor {
|
||||
return this
|
||||
}
|
||||
|
||||
get languageMode () {
|
||||
return this.tokenizedBuffer
|
||||
}
|
||||
get languageMode () { return this.buffer.getLanguageMode() }
|
||||
get tokenizedBuffer () { return this.buffer.getLanguageMode() }
|
||||
|
||||
get rowsPerPage () {
|
||||
return this.getRowsPerPage()
|
||||
@@ -319,10 +325,6 @@ class TextEditor {
|
||||
this.undoGroupingInterval = value
|
||||
break
|
||||
|
||||
case 'nonWordCharacters':
|
||||
this.nonWordCharacters = value
|
||||
break
|
||||
|
||||
case 'scrollSensitivity':
|
||||
this.scrollSensitivity = value
|
||||
break
|
||||
@@ -344,8 +346,7 @@ class TextEditor {
|
||||
break
|
||||
|
||||
case 'tabLength':
|
||||
if (value > 0 && value !== this.tokenizedBuffer.getTabLength()) {
|
||||
this.tokenizedBuffer.setTabLength(value)
|
||||
if (value > 0 && value !== this.displayLayer.tabLength) {
|
||||
displayLayerParams.tabLength = value
|
||||
}
|
||||
break
|
||||
@@ -404,6 +405,15 @@ class TextEditor {
|
||||
}
|
||||
break
|
||||
|
||||
case 'readOnly':
|
||||
if (value !== this.readOnly) {
|
||||
this.readOnly = value
|
||||
if (this.component != null) {
|
||||
this.component.scheduleUpdate()
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case 'placeholderText':
|
||||
if (value !== this.placeholderText) {
|
||||
this.placeholderText = value
|
||||
@@ -513,34 +523,30 @@ class TextEditor {
|
||||
}
|
||||
|
||||
serialize () {
|
||||
const tokenizedBufferState = this.tokenizedBuffer.serialize()
|
||||
|
||||
return {
|
||||
deserializer: 'TextEditor',
|
||||
version: SERIALIZATION_VERSION,
|
||||
|
||||
// TODO: Remove this forward-compatible fallback once 1.8 reaches stable.
|
||||
displayBuffer: {tokenizedBuffer: tokenizedBufferState},
|
||||
|
||||
tokenizedBuffer: tokenizedBufferState,
|
||||
displayLayerId: this.displayLayer.id,
|
||||
selectionsMarkerLayerId: this.selectionsMarkerLayer.id,
|
||||
|
||||
initialScrollTopRow: this.getScrollTopRow(),
|
||||
initialScrollLeftColumn: this.getScrollLeftColumn(),
|
||||
|
||||
tabLength: this.displayLayer.tabLength,
|
||||
atomicSoftTabs: this.displayLayer.atomicSoftTabs,
|
||||
softWrapHangingIndentLength: this.displayLayer.softWrapHangingIndent,
|
||||
|
||||
id: this.id,
|
||||
bufferId: this.buffer.id,
|
||||
softTabs: this.softTabs,
|
||||
softWrapped: this.softWrapped,
|
||||
softWrapAtPreferredLineLength: this.softWrapAtPreferredLineLength,
|
||||
preferredLineLength: this.preferredLineLength,
|
||||
mini: this.mini,
|
||||
readOnly: this.readOnly,
|
||||
editorWidthInChars: this.editorWidthInChars,
|
||||
width: this.width,
|
||||
largeFileMode: this.largeFileMode,
|
||||
maxScreenLineLength: this.maxScreenLineLength,
|
||||
registered: this.registered,
|
||||
invisibles: this.invisibles,
|
||||
@@ -553,6 +559,7 @@ class TextEditor {
|
||||
|
||||
subscribeToBuffer () {
|
||||
this.buffer.retain()
|
||||
this.disposables.add(this.buffer.onDidChangeLanguageMode(this.handleLanguageModeChange.bind(this)))
|
||||
this.disposables.add(this.buffer.onDidChangePath(() => {
|
||||
this.emitter.emit('did-change-title', this.getTitle())
|
||||
this.emitter.emit('did-change-path', this.getPath())
|
||||
@@ -576,7 +583,6 @@ class TextEditor {
|
||||
}
|
||||
|
||||
subscribeToDisplayLayer () {
|
||||
this.disposables.add(this.tokenizedBuffer.onDidChangeGrammar(this.handleGrammarChange.bind(this)))
|
||||
this.disposables.add(this.displayLayer.onDidChange(changes => {
|
||||
this.mergeIntersectingSelections()
|
||||
if (this.component) this.component.didChangeDisplayLayer(changes)
|
||||
@@ -596,7 +602,6 @@ class TextEditor {
|
||||
this.alive = false
|
||||
this.disposables.dispose()
|
||||
this.displayLayer.destroy()
|
||||
this.tokenizedBuffer.destroy()
|
||||
for (let selection of this.selections.slice()) {
|
||||
selection.destroy()
|
||||
}
|
||||
@@ -731,7 +736,9 @@ class TextEditor {
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeGrammar (callback) {
|
||||
return this.emitter.on('did-change-grammar', callback)
|
||||
return this.buffer.onDidChangeLanguageMode(() => {
|
||||
callback(this.buffer.getLanguageMode().grammar)
|
||||
})
|
||||
}
|
||||
|
||||
// Extended: Calls your `callback` when the result of {::isModified} changes.
|
||||
@@ -947,7 +954,7 @@ class TextEditor {
|
||||
selectionsMarkerLayer,
|
||||
softTabs,
|
||||
suppressCursorCreation: true,
|
||||
tabLength: this.tokenizedBuffer.getTabLength(),
|
||||
tabLength: this.getTabLength(),
|
||||
initialScrollTopRow: this.getScrollTopRow(),
|
||||
initialScrollLeftColumn: this.getScrollLeftColumn(),
|
||||
assert: this.assert,
|
||||
@@ -960,7 +967,12 @@ class TextEditor {
|
||||
}
|
||||
|
||||
// Controls visibility based on the given {Boolean}.
|
||||
setVisible (visible) { this.tokenizedBuffer.setVisible(visible) }
|
||||
setVisible (visible) {
|
||||
if (visible) {
|
||||
const languageMode = this.buffer.getLanguageMode()
|
||||
if (languageMode.startTokenizing) languageMode.startTokenizing()
|
||||
}
|
||||
}
|
||||
|
||||
setMini (mini) {
|
||||
this.update({mini})
|
||||
@@ -968,6 +980,12 @@ class TextEditor {
|
||||
|
||||
isMini () { return this.mini }
|
||||
|
||||
setReadOnly (readOnly) {
|
||||
this.update({readOnly})
|
||||
}
|
||||
|
||||
isReadOnly () { return this.readOnly }
|
||||
|
||||
onDidChangeMini (callback) {
|
||||
return this.emitter.on('did-change-mini', callback)
|
||||
}
|
||||
@@ -1312,15 +1330,24 @@ class TextEditor {
|
||||
insertText (text, options = {}) {
|
||||
if (!this.emitWillInsertTextEvent(text)) return false
|
||||
|
||||
let groupLastChanges = false
|
||||
if (options.undo === 'skip') {
|
||||
options = Object.assign({}, options)
|
||||
delete options.undo
|
||||
groupLastChanges = true
|
||||
}
|
||||
|
||||
const groupingInterval = options.groupUndo ? this.undoGroupingInterval : 0
|
||||
if (options.autoIndentNewline == null) options.autoIndentNewline = this.shouldAutoIndent()
|
||||
if (options.autoDecreaseIndent == null) options.autoDecreaseIndent = this.shouldAutoIndent()
|
||||
return this.mutateSelectedText(selection => {
|
||||
const result = this.mutateSelectedText(selection => {
|
||||
const range = selection.insertText(text, options)
|
||||
const didInsertEvent = {text, range}
|
||||
this.emitter.emit('did-insert-text', didInsertEvent)
|
||||
return range
|
||||
}, groupingInterval)
|
||||
if (groupLastChanges) this.buffer.groupLastChanges()
|
||||
return result
|
||||
}
|
||||
|
||||
// Essential: For each selection, replace the selected text with a newline.
|
||||
@@ -2646,7 +2673,7 @@ class TextEditor {
|
||||
return this.cursors.slice()
|
||||
}
|
||||
|
||||
// Extended: Get all {Cursors}s, ordered by their position in the buffer
|
||||
// Extended: Get all {Cursor}s, ordered by their position in the buffer
|
||||
// instead of the order in which they were added.
|
||||
//
|
||||
// Returns an {Array} of {Selection}s.
|
||||
@@ -3056,6 +3083,36 @@ class TextEditor {
|
||||
return this.expandSelectionsBackward(selection => selection.selectToBeginningOfPreviousParagraph())
|
||||
}
|
||||
|
||||
// Extended: For each selection, select the syntax node that contains
|
||||
// that selection.
|
||||
selectLargerSyntaxNode () {
|
||||
const languageMode = this.buffer.getLanguageMode()
|
||||
if (!languageMode.getRangeForSyntaxNodeContainingRange) return
|
||||
|
||||
this.expandSelectionsForward(selection => {
|
||||
const currentRange = selection.getBufferRange()
|
||||
const newRange = languageMode.getRangeForSyntaxNodeContainingRange(currentRange)
|
||||
if (newRange) {
|
||||
if (!selection._rangeStack) selection._rangeStack = []
|
||||
selection._rangeStack.push(currentRange)
|
||||
selection.setBufferRange(newRange)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Extended: Undo the effect a preceding call to {::selectLargerSyntaxNode}.
|
||||
selectSmallerSyntaxNode () {
|
||||
this.expandSelectionsForward(selection => {
|
||||
if (selection._rangeStack) {
|
||||
const lastRange = selection._rangeStack[selection._rangeStack.length - 1]
|
||||
if (lastRange && selection.getBufferRange().containsRange(lastRange)) {
|
||||
selection._rangeStack.length--
|
||||
selection.setBufferRange(lastRange)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Extended: Select the range of the given marker if it is valid.
|
||||
//
|
||||
// * `marker` A {DisplayMarker}
|
||||
@@ -3353,7 +3410,7 @@ class TextEditor {
|
||||
// Essential: Get the on-screen length of tab characters.
|
||||
//
|
||||
// Returns a {Number}.
|
||||
getTabLength () { return this.tokenizedBuffer.getTabLength() }
|
||||
getTabLength () { return this.displayLayer.tabLength }
|
||||
|
||||
// Essential: Set the on-screen length of tab characters. Setting this to a
|
||||
// {Number} This will override the `editor.tabLength` setting.
|
||||
@@ -3384,9 +3441,10 @@ class TextEditor {
|
||||
// Returns a {Boolean} or undefined if no non-comment lines had leading
|
||||
// whitespace.
|
||||
usesSoftTabs () {
|
||||
const languageMode = this.buffer.getLanguageMode()
|
||||
const hasIsRowCommented = languageMode.isRowCommented
|
||||
for (let bufferRow = 0, end = Math.min(1000, this.buffer.getLastRow()); bufferRow <= end; bufferRow++) {
|
||||
const tokenizedLine = this.tokenizedBuffer.tokenizedLines[bufferRow]
|
||||
if (tokenizedLine && tokenizedLine.isComment()) continue
|
||||
if (hasIsRowCommented && languageMode.isRowCommented(bufferRow)) continue
|
||||
const line = this.buffer.lineForRow(bufferRow)
|
||||
if (line[0] === ' ') return true
|
||||
if (line[0] === '\t') return false
|
||||
@@ -3509,7 +3567,19 @@ class TextEditor {
|
||||
//
|
||||
// Returns a {Number}.
|
||||
indentLevelForLine (line) {
|
||||
return this.tokenizedBuffer.indentLevelForLine(line)
|
||||
const tabLength = this.getTabLength()
|
||||
let indentLength = 0
|
||||
for (let i = 0, {length} = line; i < length; i++) {
|
||||
const char = line[i]
|
||||
if (char === '\t') {
|
||||
indentLength += tabLength - (indentLength % tabLength)
|
||||
} else if (char === ' ') {
|
||||
indentLength++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return indentLength / tabLength
|
||||
}
|
||||
|
||||
// Extended: Indent rows intersecting selections based on the grammar's suggested
|
||||
@@ -3542,27 +3612,24 @@ class TextEditor {
|
||||
|
||||
// Essential: Get the current {Grammar} of this editor.
|
||||
getGrammar () {
|
||||
return this.tokenizedBuffer.grammar
|
||||
const languageMode = this.buffer.getLanguageMode()
|
||||
return languageMode.getGrammar && languageMode.getGrammar() || NullGrammar
|
||||
}
|
||||
|
||||
// Essential: Set the current {Grammar} of this editor.
|
||||
// Deprecated: Set the current {Grammar} of this editor.
|
||||
//
|
||||
// Assigning a grammar will cause the editor to re-tokenize based on the new
|
||||
// grammar.
|
||||
//
|
||||
// * `grammar` {Grammar}
|
||||
setGrammar (grammar) {
|
||||
return this.tokenizedBuffer.setGrammar(grammar)
|
||||
}
|
||||
|
||||
// Reload the grammar based on the file name.
|
||||
reloadGrammar () {
|
||||
return this.tokenizedBuffer.reloadGrammar()
|
||||
const buffer = this.getBuffer()
|
||||
buffer.setLanguageMode(atom.grammars.languageModeForGrammarAndBuffer(grammar, buffer))
|
||||
}
|
||||
|
||||
// Experimental: Get a notification when async tokenization is completed.
|
||||
onDidTokenize (callback) {
|
||||
return this.tokenizedBuffer.onDidTokenize(callback)
|
||||
return this.emitter.on('did-tokenize', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -3573,21 +3640,22 @@ class TextEditor {
|
||||
// e.g. `['.source.ruby']`, or `['.source.coffee']`. You can use this with
|
||||
// {Config::get} to get language specific config values.
|
||||
getRootScopeDescriptor () {
|
||||
return this.tokenizedBuffer.rootScopeDescriptor
|
||||
return this.buffer.getLanguageMode().rootScopeDescriptor
|
||||
}
|
||||
|
||||
// Essential: Get the syntactic scopeDescriptor for the given position in buffer
|
||||
// Essential: Get the syntactic {ScopeDescriptor} for the given position in buffer
|
||||
// coordinates. Useful with {Config::get}.
|
||||
//
|
||||
// For example, if called with a position inside the parameter list of an
|
||||
// anonymous CoffeeScript function, the method returns the following array:
|
||||
// `["source.coffee", "meta.inline.function.coffee", "variable.parameter.function.coffee"]`
|
||||
// anonymous CoffeeScript function, this method returns a {ScopeDescriptor} with
|
||||
// the following scopes array:
|
||||
// `["source.coffee", "meta.function.inline.coffee", "meta.parameters.coffee", "variable.parameter.function.coffee"]`
|
||||
//
|
||||
// * `bufferPosition` A {Point} or {Array} of [row, column].
|
||||
// * `bufferPosition` A {Point} or {Array} of `[row, column]`.
|
||||
//
|
||||
// Returns a {ScopeDescriptor}.
|
||||
scopeDescriptorForBufferPosition (bufferPosition) {
|
||||
return this.tokenizedBuffer.scopeDescriptorForPosition(bufferPosition)
|
||||
return this.buffer.getLanguageMode().scopeDescriptorForPosition(bufferPosition)
|
||||
}
|
||||
|
||||
// Extended: Get the range in buffer coordinates of all tokens surrounding the
|
||||
@@ -3604,7 +3672,7 @@ class TextEditor {
|
||||
}
|
||||
|
||||
bufferRangeForScopeAtPosition (scopeSelector, position) {
|
||||
return this.tokenizedBuffer.bufferRangeForScopeAtPosition(scopeSelector, position)
|
||||
return this.buffer.getLanguageMode().bufferRangeForScopeAtPosition(scopeSelector, position)
|
||||
}
|
||||
|
||||
// Extended: Determine if the given row is entirely a comment
|
||||
@@ -3622,7 +3690,7 @@ class TextEditor {
|
||||
}
|
||||
|
||||
tokenForBufferPosition (bufferPosition) {
|
||||
return this.tokenizedBuffer.tokenForPosition(bufferPosition)
|
||||
return this.buffer.getLanguageMode().tokenForPosition(bufferPosition)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -3749,20 +3817,18 @@ class TextEditor {
|
||||
// level.
|
||||
foldCurrentRow () {
|
||||
const {row} = this.getCursorBufferPosition()
|
||||
const range = this.tokenizedBuffer.getFoldableRangeContainingPoint(Point(row, Infinity))
|
||||
if (range) {
|
||||
const result = this.displayLayer.foldBufferRange(range)
|
||||
this.scrollToCursorPosition()
|
||||
return result
|
||||
}
|
||||
const languageMode = this.buffer.getLanguageMode()
|
||||
const range = (
|
||||
languageMode.getFoldableRangeContainingPoint &&
|
||||
languageMode.getFoldableRangeContainingPoint(Point(row, Infinity), this.getTabLength())
|
||||
)
|
||||
if (range) return this.displayLayer.foldBufferRange(range)
|
||||
}
|
||||
|
||||
// Essential: Unfold the most recent cursor's row by one level.
|
||||
unfoldCurrentRow () {
|
||||
const {row} = this.getCursorBufferPosition()
|
||||
const result = this.displayLayer.destroyFoldsContainingBufferPositions([Point(row, Infinity)], false)
|
||||
this.scrollToCursorPosition()
|
||||
return result
|
||||
return this.displayLayer.destroyFoldsContainingBufferPositions([Point(row, Infinity)], false)
|
||||
}
|
||||
|
||||
// Essential: Fold the given row in buffer coordinates based on its indentation
|
||||
@@ -3774,13 +3840,16 @@ class TextEditor {
|
||||
// * `bufferRow` A {Number}.
|
||||
foldBufferRow (bufferRow) {
|
||||
let position = Point(bufferRow, Infinity)
|
||||
const languageMode = this.buffer.getLanguageMode()
|
||||
while (true) {
|
||||
const foldableRange = this.tokenizedBuffer.getFoldableRangeContainingPoint(position, this.getTabLength())
|
||||
const foldableRange = (
|
||||
languageMode.getFoldableRangeContainingPoint &&
|
||||
languageMode.getFoldableRangeContainingPoint(position, this.getTabLength())
|
||||
)
|
||||
if (foldableRange) {
|
||||
const existingFolds = this.displayLayer.foldsIntersectingBufferRange(Range(foldableRange.start, foldableRange.start))
|
||||
if (existingFolds.length === 0) {
|
||||
this.displayLayer.foldBufferRange(foldableRange)
|
||||
this.scrollToCursorPosition()
|
||||
} else {
|
||||
const firstExistingFoldRange = this.displayLayer.bufferRangeForFold(existingFolds[0])
|
||||
if (firstExistingFoldRange.start.isLessThan(position)) {
|
||||
@@ -3798,9 +3867,7 @@ class TextEditor {
|
||||
// * `bufferRow` A {Number}
|
||||
unfoldBufferRow (bufferRow) {
|
||||
const position = Point(bufferRow, Infinity)
|
||||
const result = this.displayLayer.destroyFoldsContainingBufferPositions([position])
|
||||
this.scrollToCursorPosition()
|
||||
return result
|
||||
return this.displayLayer.destroyFoldsContainingBufferPositions([position])
|
||||
}
|
||||
|
||||
// Extended: For each selection, fold the rows it intersects.
|
||||
@@ -3812,11 +3879,15 @@ class TextEditor {
|
||||
|
||||
// Extended: Fold all foldable lines.
|
||||
foldAll () {
|
||||
const languageMode = this.buffer.getLanguageMode()
|
||||
const foldableRanges = (
|
||||
languageMode.getFoldableRanges &&
|
||||
languageMode.getFoldableRanges(this.getTabLength())
|
||||
)
|
||||
this.displayLayer.destroyAllFolds()
|
||||
for (let range of this.tokenizedBuffer.getFoldableRanges(this.getTabLength())) {
|
||||
for (let range of foldableRanges || []) {
|
||||
this.displayLayer.foldBufferRange(range)
|
||||
}
|
||||
this.scrollToCursorPosition()
|
||||
}
|
||||
|
||||
// Extended: Unfold all existing folds.
|
||||
@@ -3828,13 +3899,17 @@ class TextEditor {
|
||||
|
||||
// Extended: Fold all foldable lines at the given indent level.
|
||||
//
|
||||
// * `level` A {Number}.
|
||||
// * `level` A {Number} starting at 0.
|
||||
foldAllAtIndentLevel (level) {
|
||||
const languageMode = this.buffer.getLanguageMode()
|
||||
const foldableRanges = (
|
||||
languageMode.getFoldableRangesAtIndentLevel &&
|
||||
languageMode.getFoldableRangesAtIndentLevel(level, this.getTabLength())
|
||||
)
|
||||
this.displayLayer.destroyAllFolds()
|
||||
for (let range of this.tokenizedBuffer.getFoldableRangesAtIndentLevel(level, this.getTabLength())) {
|
||||
for (let range of foldableRanges || []) {
|
||||
this.displayLayer.foldBufferRange(range)
|
||||
}
|
||||
this.scrollToCursorPosition()
|
||||
}
|
||||
|
||||
// Extended: Determine whether the given row in buffer coordinates is foldable.
|
||||
@@ -3845,7 +3920,8 @@ class TextEditor {
|
||||
//
|
||||
// Returns a {Boolean}.
|
||||
isFoldableAtBufferRow (bufferRow) {
|
||||
return this.tokenizedBuffer.isFoldableAtRow(bufferRow)
|
||||
const languageMode = this.buffer.getLanguageMode()
|
||||
return languageMode.isFoldableAtRow && languageMode.isFoldableAtRow(bufferRow)
|
||||
}
|
||||
|
||||
// Extended: Determine whether the given row in screen coordinates is foldable.
|
||||
@@ -3862,14 +3938,11 @@ class TextEditor {
|
||||
// Extended: Fold the given buffer row if it isn't currently folded, and unfold
|
||||
// it otherwise.
|
||||
toggleFoldAtBufferRow (bufferRow) {
|
||||
let result
|
||||
if (this.isFoldedAtBufferRow(bufferRow)) {
|
||||
result = this.unfoldBufferRow(bufferRow)
|
||||
return this.unfoldBufferRow(bufferRow)
|
||||
} else {
|
||||
result = this.foldBufferRow(bufferRow)
|
||||
return this.foldBufferRow(bufferRow)
|
||||
}
|
||||
this.scrollToCursorPosition()
|
||||
return result
|
||||
}
|
||||
|
||||
// Extended: Determine whether the most recently added cursor's row is folded.
|
||||
@@ -3908,9 +3981,7 @@ class TextEditor {
|
||||
//
|
||||
// Returns the new {Fold}.
|
||||
foldBufferRowRange (startRow, endRow) {
|
||||
const result = this.foldBufferRange(Range(Point(startRow, Infinity), Point(endRow, Infinity)))
|
||||
this.scrollToCursorPosition()
|
||||
return result
|
||||
return this.foldBufferRange(Range(Point(startRow, Infinity), Point(endRow, Infinity)))
|
||||
}
|
||||
|
||||
foldBufferRange (range) {
|
||||
@@ -4055,18 +4126,6 @@ class TextEditor {
|
||||
Section: Config
|
||||
*/
|
||||
|
||||
// Experimental: Supply an object that will provide the editor with settings
|
||||
// for specific syntactic scopes. See the `ScopedSettingsDelegate` in
|
||||
// `text-editor-registry.js` for an example implementation.
|
||||
setScopedSettingsDelegate (scopedSettingsDelegate) {
|
||||
this.scopedSettingsDelegate = scopedSettingsDelegate
|
||||
this.tokenizedBuffer.scopedSettingsDelegate = this.scopedSettingsDelegate
|
||||
}
|
||||
|
||||
// Experimental: Retrieve the {Object} that provides the editor with settings
|
||||
// for specific syntactic scopes.
|
||||
getScopedSettingsDelegate () { return this.scopedSettingsDelegate }
|
||||
|
||||
// Experimental: Is auto-indentation enabled for this editor?
|
||||
//
|
||||
// Returns a {Boolean}.
|
||||
@@ -4114,21 +4173,34 @@ class TextEditor {
|
||||
// for the purpose of word-based cursor movements.
|
||||
//
|
||||
// Returns a {String} containing the non-word characters.
|
||||
getNonWordCharacters (scopes) {
|
||||
if (this.scopedSettingsDelegate && this.scopedSettingsDelegate.getNonWordCharacters) {
|
||||
return this.scopedSettingsDelegate.getNonWordCharacters(scopes) || this.nonWordCharacters
|
||||
} else {
|
||||
return this.nonWordCharacters
|
||||
}
|
||||
getNonWordCharacters (position) {
|
||||
const languageMode = this.buffer.getLanguageMode()
|
||||
return (
|
||||
languageMode.getNonWordCharacters &&
|
||||
languageMode.getNonWordCharacters(position || Point(0, 0))
|
||||
) || DEFAULT_NON_WORD_CHARACTERS
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Event Handlers
|
||||
*/
|
||||
|
||||
handleGrammarChange () {
|
||||
handleLanguageModeChange () {
|
||||
this.unfoldAll()
|
||||
return this.emitter.emit('did-change-grammar', this.getGrammar())
|
||||
if (this.languageModeSubscription) {
|
||||
this.languageModeSubscription.dispose()
|
||||
this.disposables.remove(this.languageModeSubscription)
|
||||
}
|
||||
const languageMode = this.buffer.getLanguageMode()
|
||||
|
||||
if (this.component && this.component.visible && languageMode.startTokenizing) {
|
||||
languageMode.startTokenizing()
|
||||
}
|
||||
this.languageModeSubscription = languageMode.onDidTokenize && languageMode.onDidTokenize(() => {
|
||||
this.emitter.emit('did-tokenize')
|
||||
})
|
||||
if (this.languageModeSubscription) this.disposables.add(this.languageModeSubscription)
|
||||
this.emitter.emit('did-change-grammar', languageMode.grammar)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -4398,7 +4470,11 @@ class TextEditor {
|
||||
*/
|
||||
|
||||
suggestedIndentForBufferRow (bufferRow, options) {
|
||||
return this.tokenizedBuffer.suggestedIndentForBufferRow(bufferRow, options)
|
||||
const languageMode = this.buffer.getLanguageMode()
|
||||
return (
|
||||
languageMode.suggestedIndentForBufferRow &&
|
||||
languageMode.suggestedIndentForBufferRow(bufferRow, this.getTabLength(), options)
|
||||
)
|
||||
}
|
||||
|
||||
// Given a buffer row, indent it.
|
||||
@@ -4423,17 +4499,21 @@ class TextEditor {
|
||||
}
|
||||
|
||||
autoDecreaseIndentForBufferRow (bufferRow) {
|
||||
const indentLevel = this.tokenizedBuffer.suggestedIndentForEditedBufferRow(bufferRow)
|
||||
const languageMode = this.buffer.getLanguageMode()
|
||||
const indentLevel = (
|
||||
languageMode.suggestedIndentForEditedBufferRow &&
|
||||
languageMode.suggestedIndentForEditedBufferRow(bufferRow, this.getTabLength())
|
||||
)
|
||||
if (indentLevel != null) this.setIndentationForBufferRow(bufferRow, indentLevel)
|
||||
}
|
||||
|
||||
toggleLineCommentForBufferRow (row) { this.toggleLineCommentsForBufferRows(row, row) }
|
||||
|
||||
toggleLineCommentsForBufferRows (start, end) {
|
||||
let {
|
||||
commentStartString,
|
||||
commentEndString
|
||||
} = this.tokenizedBuffer.commentStringsForPosition(Point(start, 0))
|
||||
const languageMode = this.buffer.getLanguageMode()
|
||||
let {commentStartString, commentEndString} =
|
||||
languageMode.commentStringsForPosition &&
|
||||
languageMode.commentStringsForPosition(Point(start, 0)) || {}
|
||||
if (!commentStartString) return
|
||||
commentStartString = commentStartString.trim()
|
||||
|
||||
@@ -4503,8 +4583,7 @@ class TextEditor {
|
||||
? minBlankIndentLevel
|
||||
: 0
|
||||
|
||||
const tabLength = this.getTabLength()
|
||||
const indentString = ' '.repeat(tabLength * minIndentLevel)
|
||||
const indentString = this.buildIndentString(minIndentLevel)
|
||||
for (let row = start; row <= end; row++) {
|
||||
const line = this.buffer.lineForRow(row)
|
||||
if (NON_WHITESPACE_REGEXP.test(line)) {
|
||||
@@ -4524,12 +4603,13 @@ class TextEditor {
|
||||
rowRangeForParagraphAtBufferRow (bufferRow) {
|
||||
if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(bufferRow))) return
|
||||
|
||||
const isCommented = this.tokenizedBuffer.isRowCommented(bufferRow)
|
||||
const languageMode = this.buffer.getLanguageMode()
|
||||
const isCommented = languageMode.isRowCommented(bufferRow)
|
||||
|
||||
let startRow = bufferRow
|
||||
while (startRow > 0) {
|
||||
if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(startRow - 1))) break
|
||||
if (this.tokenizedBuffer.isRowCommented(startRow - 1) !== isCommented) break
|
||||
if (languageMode.isRowCommented(startRow - 1) !== isCommented) break
|
||||
startRow--
|
||||
}
|
||||
|
||||
@@ -4537,7 +4617,7 @@ class TextEditor {
|
||||
const rowCount = this.getLineCount()
|
||||
while (endRow < rowCount) {
|
||||
if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(endRow + 1))) break
|
||||
if (this.tokenizedBuffer.isRowCommented(endRow + 1) !== isCommented) break
|
||||
if (languageMode.isRowCommented(endRow + 1) !== isCommented) break
|
||||
endRow++
|
||||
}
|
||||
|
||||
|
||||
@@ -4,27 +4,16 @@ const {Point, Range} = require('text-buffer')
|
||||
const TokenizedLine = require('./tokenized-line')
|
||||
const TokenIterator = require('./token-iterator')
|
||||
const ScopeDescriptor = require('./scope-descriptor')
|
||||
const TokenizedBufferIterator = require('./tokenized-buffer-iterator')
|
||||
const NullGrammar = require('./null-grammar')
|
||||
const {OnigRegExp} = require('oniguruma')
|
||||
const {toFirstMateScopeId} = require('./first-mate-helpers')
|
||||
const {toFirstMateScopeId, fromFirstMateScopeId} = require('./first-mate-helpers')
|
||||
|
||||
const NON_WHITESPACE_REGEX = /\S/
|
||||
|
||||
let nextId = 0
|
||||
const prefixedScopes = new Map()
|
||||
|
||||
module.exports =
|
||||
class TokenizedBuffer {
|
||||
static deserialize (state, atomEnvironment) {
|
||||
const buffer = atomEnvironment.project.bufferForIdSync(state.bufferId)
|
||||
if (!buffer) return null
|
||||
|
||||
state.buffer = buffer
|
||||
state.assert = atomEnvironment.assert
|
||||
return new TokenizedBuffer(state)
|
||||
}
|
||||
|
||||
class TextMateLanguageMode {
|
||||
constructor (params) {
|
||||
this.emitter = new Emitter()
|
||||
this.disposables = new CompositeDisposable()
|
||||
@@ -32,16 +21,19 @@ class TokenizedBuffer {
|
||||
this.regexesByPattern = {}
|
||||
|
||||
this.alive = true
|
||||
this.visible = false
|
||||
this.tokenizationStarted = false
|
||||
this.id = params.id != null ? params.id : nextId++
|
||||
this.buffer = params.buffer
|
||||
this.tabLength = params.tabLength
|
||||
this.largeFileMode = params.largeFileMode
|
||||
this.assert = params.assert
|
||||
this.scopedSettingsDelegate = params.scopedSettingsDelegate
|
||||
this.config = params.config
|
||||
this.largeFileMode = params.largeFileMode != null
|
||||
? params.largeFileMode
|
||||
: this.buffer.buffer.getLength() >= 2 * 1024 * 1024
|
||||
|
||||
this.setGrammar(params.grammar || NullGrammar)
|
||||
this.disposables.add(this.buffer.registerTextDecorationLayer(this))
|
||||
this.grammar = params.grammar || NullGrammar
|
||||
this.rootScopeDescriptor = new ScopeDescriptor({scopes: [this.grammar.scopeName]})
|
||||
this.disposables.add(this.grammar.onDidUpdate(() => this.retokenizeLines()))
|
||||
this.retokenizeLines()
|
||||
}
|
||||
|
||||
destroy () {
|
||||
@@ -59,6 +51,19 @@ class TokenizedBuffer {
|
||||
return !this.alive
|
||||
}
|
||||
|
||||
getGrammar () {
|
||||
return this.grammar
|
||||
}
|
||||
|
||||
getLanguageId () {
|
||||
return this.grammar.scopeName
|
||||
}
|
||||
|
||||
getNonWordCharacters (position) {
|
||||
const scope = this.scopeDescriptorForPosition(position)
|
||||
return this.config.get('editor.nonWordCharacters', {scope})
|
||||
}
|
||||
|
||||
/*
|
||||
Section - auto-indent
|
||||
*/
|
||||
@@ -68,10 +73,19 @@ class TokenizedBuffer {
|
||||
// * bufferRow - A {Number} indicating the buffer row
|
||||
//
|
||||
// Returns a {Number}.
|
||||
suggestedIndentForBufferRow (bufferRow, options) {
|
||||
suggestedIndentForBufferRow (bufferRow, tabLength, options) {
|
||||
const line = this.buffer.lineForRow(bufferRow)
|
||||
const tokenizedLine = this.tokenizedLineForRow(bufferRow)
|
||||
return this._suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
|
||||
const iterator = tokenizedLine.getTokenIterator()
|
||||
iterator.next()
|
||||
const scopeDescriptor = new ScopeDescriptor({scopes: iterator.getScopes()})
|
||||
return this._suggestedIndentForLineWithScopeAtBufferRow(
|
||||
bufferRow,
|
||||
line,
|
||||
scopeDescriptor,
|
||||
tabLength,
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
// Get the suggested indentation level for a given line of text, if it were inserted at the given
|
||||
@@ -80,9 +94,17 @@ class TokenizedBuffer {
|
||||
// * bufferRow - A {Number} indicating the buffer row
|
||||
//
|
||||
// Returns a {Number}.
|
||||
suggestedIndentForLineAtBufferRow (bufferRow, line, options) {
|
||||
suggestedIndentForLineAtBufferRow (bufferRow, line, tabLength) {
|
||||
const tokenizedLine = this.buildTokenizedLineForRowWithText(bufferRow, line)
|
||||
return this._suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
|
||||
const iterator = tokenizedLine.getTokenIterator()
|
||||
iterator.next()
|
||||
const scopeDescriptor = new ScopeDescriptor({scopes: iterator.getScopes()})
|
||||
return this._suggestedIndentForLineWithScopeAtBufferRow(
|
||||
bufferRow,
|
||||
line,
|
||||
scopeDescriptor,
|
||||
tabLength
|
||||
)
|
||||
}
|
||||
|
||||
// Get the suggested indentation level for a line in the buffer on which the user is currently
|
||||
@@ -93,12 +115,12 @@ class TokenizedBuffer {
|
||||
// * bufferRow - The row {Number}
|
||||
//
|
||||
// Returns a {Number}.
|
||||
suggestedIndentForEditedBufferRow (bufferRow) {
|
||||
suggestedIndentForEditedBufferRow (bufferRow, tabLength) {
|
||||
const line = this.buffer.lineForRow(bufferRow)
|
||||
const currentIndentLevel = this.indentLevelForLine(line)
|
||||
const currentIndentLevel = this.indentLevelForLine(line, tabLength)
|
||||
if (currentIndentLevel === 0) return
|
||||
|
||||
const scopeDescriptor = this.scopeDescriptorForPosition([bufferRow, 0])
|
||||
const scopeDescriptor = this.scopeDescriptorForPosition(new Point(bufferRow, 0))
|
||||
const decreaseIndentRegex = this.decreaseIndentRegexForScopeDescriptor(scopeDescriptor)
|
||||
if (!decreaseIndentRegex) return
|
||||
|
||||
@@ -108,7 +130,7 @@ class TokenizedBuffer {
|
||||
if (precedingRow == null) return
|
||||
|
||||
const precedingLine = this.buffer.lineForRow(precedingRow)
|
||||
let desiredIndentLevel = this.indentLevelForLine(precedingLine)
|
||||
let desiredIndentLevel = this.indentLevelForLine(precedingLine, tabLength)
|
||||
|
||||
const increaseIndentRegex = this.increaseIndentRegexForScopeDescriptor(scopeDescriptor)
|
||||
if (increaseIndentRegex) {
|
||||
@@ -125,11 +147,7 @@ class TokenizedBuffer {
|
||||
return desiredIndentLevel
|
||||
}
|
||||
|
||||
_suggestedIndentForTokenizedLineAtBufferRow (bufferRow, line, tokenizedLine, options) {
|
||||
const iterator = tokenizedLine.getTokenIterator()
|
||||
iterator.next()
|
||||
const scopeDescriptor = new ScopeDescriptor({scopes: iterator.getScopes()})
|
||||
|
||||
_suggestedIndentForLineWithScopeAtBufferRow (bufferRow, line, scopeDescriptor, tabLength, options) {
|
||||
const increaseIndentRegex = this.increaseIndentRegexForScopeDescriptor(scopeDescriptor)
|
||||
const decreaseIndentRegex = this.decreaseIndentRegexForScopeDescriptor(scopeDescriptor)
|
||||
const decreaseNextIndentRegex = this.decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor)
|
||||
@@ -144,7 +162,7 @@ class TokenizedBuffer {
|
||||
}
|
||||
|
||||
const precedingLine = this.buffer.lineForRow(precedingRow)
|
||||
let desiredIndentLevel = this.indentLevelForLine(precedingLine)
|
||||
let desiredIndentLevel = this.indentLevelForLine(precedingLine, tabLength)
|
||||
if (!increaseIndentRegex) return desiredIndentLevel
|
||||
|
||||
if (!this.isRowCommented(precedingRow)) {
|
||||
@@ -164,16 +182,25 @@ class TokenizedBuffer {
|
||||
*/
|
||||
|
||||
commentStringsForPosition (position) {
|
||||
if (this.scopedSettingsDelegate) {
|
||||
const scope = this.scopeDescriptorForPosition(position)
|
||||
return this.scopedSettingsDelegate.getCommentStrings(scope)
|
||||
} else {
|
||||
return {}
|
||||
const scope = this.scopeDescriptorForPosition(position)
|
||||
const commentStartEntries = this.config.getAll('editor.commentStart', {scope})
|
||||
const commentEndEntries = this.config.getAll('editor.commentEnd', {scope})
|
||||
const commentStartEntry = commentStartEntries[0]
|
||||
const commentEndEntry = commentEndEntries.find((entry) => {
|
||||
return entry.scopeSelector === commentStartEntry.scopeSelector
|
||||
})
|
||||
return {
|
||||
commentStartString: commentStartEntry && commentStartEntry.value,
|
||||
commentEndString: commentEndEntry && commentEndEntry.value
|
||||
}
|
||||
}
|
||||
|
||||
buildIterator () {
|
||||
return new TokenizedBufferIterator(this)
|
||||
/*
|
||||
Section - Syntax Highlighting
|
||||
*/
|
||||
|
||||
buildHighlightIterator () {
|
||||
return new TextMateHighlightIterator(this)
|
||||
}
|
||||
|
||||
classNameForScopeId (id) {
|
||||
@@ -196,47 +223,14 @@ class TokenizedBuffer {
|
||||
return []
|
||||
}
|
||||
|
||||
onDidInvalidateRange (fn) {
|
||||
return this.emitter.on('did-invalidate-range', fn)
|
||||
}
|
||||
|
||||
serialize () {
|
||||
return {
|
||||
deserializer: 'TokenizedBuffer',
|
||||
bufferPath: this.buffer.getPath(),
|
||||
bufferId: this.buffer.getId(),
|
||||
tabLength: this.tabLength,
|
||||
largeFileMode: this.largeFileMode
|
||||
}
|
||||
}
|
||||
|
||||
observeGrammar (callback) {
|
||||
callback(this.grammar)
|
||||
return this.onDidChangeGrammar(callback)
|
||||
}
|
||||
|
||||
onDidChangeGrammar (callback) {
|
||||
return this.emitter.on('did-change-grammar', callback)
|
||||
onDidChangeHighlighting (fn) {
|
||||
return this.emitter.on('did-change-highlighting', fn)
|
||||
}
|
||||
|
||||
onDidTokenize (callback) {
|
||||
return this.emitter.on('did-tokenize', callback)
|
||||
}
|
||||
|
||||
setGrammar (grammar) {
|
||||
if (!grammar || grammar === this.grammar) return
|
||||
|
||||
this.grammar = grammar
|
||||
this.rootScopeDescriptor = new ScopeDescriptor({scopes: [this.grammar.scopeName]})
|
||||
|
||||
if (this.grammarUpdateDisposable) this.grammarUpdateDisposable.dispose()
|
||||
this.grammarUpdateDisposable = this.grammar.onDidUpdate(() => this.retokenizeLines())
|
||||
this.disposables.add(this.grammarUpdateDisposable)
|
||||
|
||||
this.retokenizeLines()
|
||||
this.emitter.emit('did-change-grammar', grammar)
|
||||
}
|
||||
|
||||
getGrammarSelectionContent () {
|
||||
return this.buffer.getTextInRange([[0, 0], [10, 0]])
|
||||
}
|
||||
@@ -264,21 +258,15 @@ class TokenizedBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
setVisible (visible) {
|
||||
this.visible = visible
|
||||
if (this.visible && this.grammar.name !== 'Null Grammar' && !this.largeFileMode) {
|
||||
startTokenizing () {
|
||||
this.tokenizationStarted = true
|
||||
if (this.grammar.name !== 'Null Grammar' && !this.largeFileMode) {
|
||||
this.tokenizeInBackground()
|
||||
}
|
||||
}
|
||||
|
||||
getTabLength () { return this.tabLength }
|
||||
|
||||
setTabLength (tabLength) {
|
||||
this.tabLength = tabLength
|
||||
}
|
||||
|
||||
tokenizeInBackground () {
|
||||
if (!this.visible || this.pendingChunk || !this.alive) return
|
||||
if (!this.tokenizationStarted || this.pendingChunk || !this.alive) return
|
||||
|
||||
this.pendingChunk = true
|
||||
_.defer(() => {
|
||||
@@ -316,7 +304,7 @@ class TokenizedBuffer {
|
||||
this.validateRow(endRow)
|
||||
if (!filledRegion) this.invalidateRow(endRow + 1)
|
||||
|
||||
this.emitter.emit('did-invalidate-range', Range(Point(startRow, 0), Point(endRow + 1, 0)))
|
||||
this.emitter.emit('did-change-highlighting', Range(Point(startRow, 0), Point(endRow + 1, 0)))
|
||||
}
|
||||
|
||||
if (this.firstInvalidRow() != null) {
|
||||
@@ -486,18 +474,6 @@ class TokenizedBuffer {
|
||||
while (true) {
|
||||
if (scopes.pop() === matchingStartTag) break
|
||||
if (scopes.length === 0) {
|
||||
this.assert(false, 'Encountered an unmatched scope end tag.', error => {
|
||||
error.metadata = {
|
||||
grammarScopeName: this.grammar.scopeName,
|
||||
unmatchedEndTag: this.grammar.scopeForId(tag)
|
||||
}
|
||||
const path = require('path')
|
||||
error.privateMetadataDescription = `The contents of \`${path.basename(this.buffer.getPath())}\``
|
||||
error.privateMetadata = {
|
||||
filePath: this.buffer.getPath(),
|
||||
fileContents: this.buffer.getText()
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -507,7 +483,7 @@ class TokenizedBuffer {
|
||||
return scopes
|
||||
}
|
||||
|
||||
indentLevelForLine (line, tabLength = this.tabLength) {
|
||||
indentLevelForLine (line, tabLength) {
|
||||
let indentLength = 0
|
||||
for (let i = 0, {length} = line; i < length; i++) {
|
||||
const char = line[i]
|
||||
@@ -629,7 +605,7 @@ class TokenizedBuffer {
|
||||
|
||||
for (let row = point.row - 1; row >= 0; row--) {
|
||||
const endRow = this.endRowForFoldAtRow(row, tabLength)
|
||||
if (endRow != null && endRow > point.row) {
|
||||
if (endRow != null && endRow >= point.row) {
|
||||
return Range(Point(row, Infinity), Point(endRow, Infinity))
|
||||
}
|
||||
}
|
||||
@@ -712,28 +688,20 @@ class TokenizedBuffer {
|
||||
return foldEndRow
|
||||
}
|
||||
|
||||
increaseIndentRegexForScopeDescriptor (scopeDescriptor) {
|
||||
if (this.scopedSettingsDelegate) {
|
||||
return this.regexForPattern(this.scopedSettingsDelegate.getIncreaseIndentPattern(scopeDescriptor))
|
||||
}
|
||||
increaseIndentRegexForScopeDescriptor (scope) {
|
||||
return this.regexForPattern(this.config.get('editor.increaseIndentPattern', {scope}))
|
||||
}
|
||||
|
||||
decreaseIndentRegexForScopeDescriptor (scopeDescriptor) {
|
||||
if (this.scopedSettingsDelegate) {
|
||||
return this.regexForPattern(this.scopedSettingsDelegate.getDecreaseIndentPattern(scopeDescriptor))
|
||||
}
|
||||
decreaseIndentRegexForScopeDescriptor (scope) {
|
||||
return this.regexForPattern(this.config.get('editor.decreaseIndentPattern', {scope}))
|
||||
}
|
||||
|
||||
decreaseNextIndentRegexForScopeDescriptor (scopeDescriptor) {
|
||||
if (this.scopedSettingsDelegate) {
|
||||
return this.regexForPattern(this.scopedSettingsDelegate.getDecreaseNextIndentPattern(scopeDescriptor))
|
||||
}
|
||||
decreaseNextIndentRegexForScopeDescriptor (scope) {
|
||||
return this.regexForPattern(this.config.get('editor.decreaseNextIndentPattern', {scope}))
|
||||
}
|
||||
|
||||
foldEndRegexForScopeDescriptor (scopes) {
|
||||
if (this.scopedSettingsDelegate) {
|
||||
return this.regexForPattern(this.scopedSettingsDelegate.getFoldEndPattern(scopes))
|
||||
}
|
||||
foldEndRegexForScopeDescriptor (scope) {
|
||||
return this.regexForPattern(this.config.get('editor.foldEndPattern', {scope}))
|
||||
}
|
||||
|
||||
regexForPattern (pattern) {
|
||||
@@ -753,7 +721,7 @@ class TokenizedBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.prototype.chunkSize = 50
|
||||
TextMateLanguageMode.prototype.chunkSize = 50
|
||||
|
||||
function selectorMatchesAnyScope (selector, scopes) {
|
||||
const targetClasses = selector.replace(/^\./, '').split('.')
|
||||
@@ -762,3 +730,142 @@ function selectorMatchesAnyScope (selector, scopes) {
|
||||
return _.isSubset(targetClasses, scopeClasses)
|
||||
})
|
||||
}
|
||||
|
||||
class TextMateHighlightIterator {
|
||||
constructor (languageMode) {
|
||||
this.languageMode = languageMode
|
||||
this.openScopeIds = null
|
||||
this.closeScopeIds = null
|
||||
}
|
||||
|
||||
seek (position) {
|
||||
this.openScopeIds = []
|
||||
this.closeScopeIds = []
|
||||
this.tagIndex = null
|
||||
|
||||
const currentLine = this.languageMode.tokenizedLineForRow(position.row)
|
||||
this.currentLineTags = currentLine.tags
|
||||
this.currentLineLength = currentLine.text.length
|
||||
const containingScopeIds = currentLine.openScopes.map((id) => fromFirstMateScopeId(id))
|
||||
|
||||
let currentColumn = 0
|
||||
for (let index = 0; index < this.currentLineTags.length; index++) {
|
||||
const tag = this.currentLineTags[index]
|
||||
if (tag >= 0) {
|
||||
if (currentColumn >= position.column) {
|
||||
this.tagIndex = index
|
||||
break
|
||||
} else {
|
||||
currentColumn += tag
|
||||
while (this.closeScopeIds.length > 0) {
|
||||
this.closeScopeIds.shift()
|
||||
containingScopeIds.pop()
|
||||
}
|
||||
while (this.openScopeIds.length > 0) {
|
||||
const openTag = this.openScopeIds.shift()
|
||||
containingScopeIds.push(openTag)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const scopeId = fromFirstMateScopeId(tag)
|
||||
if ((tag & 1) === 0) {
|
||||
if (this.openScopeIds.length > 0) {
|
||||
if (currentColumn >= position.column) {
|
||||
this.tagIndex = index
|
||||
break
|
||||
} else {
|
||||
while (this.closeScopeIds.length > 0) {
|
||||
this.closeScopeIds.shift()
|
||||
containingScopeIds.pop()
|
||||
}
|
||||
while (this.openScopeIds.length > 0) {
|
||||
const openTag = this.openScopeIds.shift()
|
||||
containingScopeIds.push(openTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.closeScopeIds.push(scopeId)
|
||||
} else {
|
||||
this.openScopeIds.push(scopeId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.tagIndex == null) {
|
||||
this.tagIndex = this.currentLineTags.length
|
||||
}
|
||||
this.position = Point(position.row, Math.min(this.currentLineLength, currentColumn))
|
||||
return containingScopeIds
|
||||
}
|
||||
|
||||
moveToSuccessor () {
|
||||
this.openScopeIds = []
|
||||
this.closeScopeIds = []
|
||||
while (true) {
|
||||
if (this.tagIndex === this.currentLineTags.length) {
|
||||
if (this.isAtTagBoundary()) {
|
||||
break
|
||||
} else if (!this.moveToNextLine()) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
const tag = this.currentLineTags[this.tagIndex]
|
||||
if (tag >= 0) {
|
||||
if (this.isAtTagBoundary()) {
|
||||
break
|
||||
} else {
|
||||
this.position = Point(this.position.row, Math.min(
|
||||
this.currentLineLength,
|
||||
this.position.column + this.currentLineTags[this.tagIndex]
|
||||
))
|
||||
}
|
||||
} else {
|
||||
const scopeId = fromFirstMateScopeId(tag)
|
||||
if ((tag & 1) === 0) {
|
||||
if (this.openScopeIds.length > 0) {
|
||||
break
|
||||
} else {
|
||||
this.closeScopeIds.push(scopeId)
|
||||
}
|
||||
} else {
|
||||
this.openScopeIds.push(scopeId)
|
||||
}
|
||||
}
|
||||
this.tagIndex++
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
getPosition () {
|
||||
return this.position
|
||||
}
|
||||
|
||||
getCloseScopeIds () {
|
||||
return this.closeScopeIds.slice()
|
||||
}
|
||||
|
||||
getOpenScopeIds () {
|
||||
return this.openScopeIds.slice()
|
||||
}
|
||||
|
||||
moveToNextLine () {
|
||||
this.position = Point(this.position.row + 1, 0)
|
||||
const tokenizedLine = this.languageMode.tokenizedLineForRow(this.position.row)
|
||||
if (tokenizedLine == null) {
|
||||
return false
|
||||
} else {
|
||||
this.currentLineTags = tokenizedLine.tags
|
||||
this.currentLineLength = tokenizedLine.text.length
|
||||
this.tagIndex = 0
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
isAtTagBoundary () {
|
||||
return this.closeScopeIds.length > 0 || this.openScopeIds.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
TextMateLanguageMode.TextMateHighlightIterator = TextMateHighlightIterator
|
||||
module.exports = TextMateLanguageMode
|
||||
@@ -50,6 +50,8 @@ class ThemeManager {
|
||||
// updating the list of active themes have completed.
|
||||
//
|
||||
// * `callback` {Function}
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeActiveThemes (callback) {
|
||||
return this.emitter.on('did-change-active-themes', callback)
|
||||
}
|
||||
@@ -134,12 +136,12 @@ class ThemeManager {
|
||||
]
|
||||
themeNames = _.intersection(themeNames, builtInThemeNames)
|
||||
if (themeNames.length === 0) {
|
||||
themeNames = ['atom-dark-syntax', 'atom-dark-ui']
|
||||
themeNames = ['one-dark-syntax', 'one-dark-ui']
|
||||
} else if (themeNames.length === 1) {
|
||||
if (_.endsWith(themeNames[0], '-ui')) {
|
||||
themeNames.unshift('atom-dark-syntax')
|
||||
themeNames.unshift('one-dark-syntax')
|
||||
} else {
|
||||
themeNames.push('atom-dark-ui')
|
||||
themeNames.push('one-dark-ui')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports =
|
||||
class TokenIterator {
|
||||
constructor (tokenizedBuffer) {
|
||||
this.tokenizedBuffer = tokenizedBuffer
|
||||
constructor (languageMode) {
|
||||
this.languageMode = languageMode
|
||||
}
|
||||
|
||||
reset (line) {
|
||||
@@ -9,7 +9,7 @@ class TokenIterator {
|
||||
this.index = null
|
||||
this.startColumn = 0
|
||||
this.endColumn = 0
|
||||
this.scopes = this.line.openScopes.map(id => this.tokenizedBuffer.grammar.scopeForId(id))
|
||||
this.scopes = this.line.openScopes.map(id => this.languageMode.grammar.scopeForId(id))
|
||||
this.scopeStarts = this.scopes.slice()
|
||||
this.scopeEnds = []
|
||||
return this
|
||||
@@ -30,7 +30,7 @@ class TokenIterator {
|
||||
while (this.index < tags.length) {
|
||||
const tag = tags[this.index]
|
||||
if (tag < 0) {
|
||||
const scope = this.tokenizedBuffer.grammar.scopeForId(tag)
|
||||
const scope = this.languageMode.grammar.scopeForId(tag)
|
||||
if ((tag % 2) === 0) {
|
||||
if (this.scopeStarts[this.scopeStarts.length - 1] === scope) {
|
||||
this.scopeStarts.pop()
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
const {Point} = require('text-buffer')
|
||||
const {fromFirstMateScopeId} = require('./first-mate-helpers')
|
||||
|
||||
module.exports = class TokenizedBufferIterator {
|
||||
constructor (tokenizedBuffer) {
|
||||
this.tokenizedBuffer = tokenizedBuffer
|
||||
this.openScopeIds = null
|
||||
this.closeScopeIds = null
|
||||
}
|
||||
|
||||
seek (position) {
|
||||
this.openScopeIds = []
|
||||
this.closeScopeIds = []
|
||||
this.tagIndex = null
|
||||
|
||||
const currentLine = this.tokenizedBuffer.tokenizedLineForRow(position.row)
|
||||
this.currentLineTags = currentLine.tags
|
||||
this.currentLineLength = currentLine.text.length
|
||||
const containingScopeIds = currentLine.openScopes.map((id) => fromFirstMateScopeId(id))
|
||||
|
||||
let currentColumn = 0
|
||||
for (let index = 0; index < this.currentLineTags.length; index++) {
|
||||
const tag = this.currentLineTags[index]
|
||||
if (tag >= 0) {
|
||||
if (currentColumn >= position.column) {
|
||||
this.tagIndex = index
|
||||
break
|
||||
} else {
|
||||
currentColumn += tag
|
||||
while (this.closeScopeIds.length > 0) {
|
||||
this.closeScopeIds.shift()
|
||||
containingScopeIds.pop()
|
||||
}
|
||||
while (this.openScopeIds.length > 0) {
|
||||
const openTag = this.openScopeIds.shift()
|
||||
containingScopeIds.push(openTag)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const scopeId = fromFirstMateScopeId(tag)
|
||||
if ((tag & 1) === 0) {
|
||||
if (this.openScopeIds.length > 0) {
|
||||
if (currentColumn >= position.column) {
|
||||
this.tagIndex = index
|
||||
break
|
||||
} else {
|
||||
while (this.closeScopeIds.length > 0) {
|
||||
this.closeScopeIds.shift()
|
||||
containingScopeIds.pop()
|
||||
}
|
||||
while (this.openScopeIds.length > 0) {
|
||||
const openTag = this.openScopeIds.shift()
|
||||
containingScopeIds.push(openTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.closeScopeIds.push(scopeId)
|
||||
} else {
|
||||
this.openScopeIds.push(scopeId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.tagIndex == null) {
|
||||
this.tagIndex = this.currentLineTags.length
|
||||
}
|
||||
this.position = Point(position.row, Math.min(this.currentLineLength, currentColumn))
|
||||
return containingScopeIds
|
||||
}
|
||||
|
||||
moveToSuccessor () {
|
||||
this.openScopeIds = []
|
||||
this.closeScopeIds = []
|
||||
while (true) {
|
||||
if (this.tagIndex === this.currentLineTags.length) {
|
||||
if (this.isAtTagBoundary()) {
|
||||
break
|
||||
} else if (!this.moveToNextLine()) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
const tag = this.currentLineTags[this.tagIndex]
|
||||
if (tag >= 0) {
|
||||
if (this.isAtTagBoundary()) {
|
||||
break
|
||||
} else {
|
||||
this.position = Point(this.position.row, Math.min(
|
||||
this.currentLineLength,
|
||||
this.position.column + this.currentLineTags[this.tagIndex]
|
||||
))
|
||||
}
|
||||
} else {
|
||||
const scopeId = fromFirstMateScopeId(tag)
|
||||
if ((tag & 1) === 0) {
|
||||
if (this.openScopeIds.length > 0) {
|
||||
break
|
||||
} else {
|
||||
this.closeScopeIds.push(scopeId)
|
||||
}
|
||||
} else {
|
||||
this.openScopeIds.push(scopeId)
|
||||
}
|
||||
}
|
||||
this.tagIndex++
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
getPosition () {
|
||||
return this.position
|
||||
}
|
||||
|
||||
getCloseScopeIds () {
|
||||
return this.closeScopeIds.slice()
|
||||
}
|
||||
|
||||
getOpenScopeIds () {
|
||||
return this.openScopeIds.slice()
|
||||
}
|
||||
|
||||
moveToNextLine () {
|
||||
this.position = Point(this.position.row + 1, 0)
|
||||
const tokenizedLine = this.tokenizedBuffer.tokenizedLineForRow(this.position.row)
|
||||
if (tokenizedLine == null) {
|
||||
return false
|
||||
} else {
|
||||
this.currentLineTags = tokenizedLine.tags
|
||||
this.currentLineLength = tokenizedLine.text.length
|
||||
this.tagIndex = 0
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
isAtTagBoundary () {
|
||||
return this.closeScopeIds.length > 0 || this.openScopeIds.length > 0
|
||||
}
|
||||
}
|
||||
72
src/tree-sitter-grammar.js
Normal file
72
src/tree-sitter-grammar.js
Normal file
@@ -0,0 +1,72 @@
|
||||
const path = require('path')
|
||||
const SyntaxScopeMap = require('./syntax-scope-map')
|
||||
const Module = require('module')
|
||||
|
||||
module.exports =
|
||||
class TreeSitterGrammar {
|
||||
constructor (registry, filePath, params) {
|
||||
this.registry = registry
|
||||
this.id = params.id
|
||||
this.name = params.name
|
||||
this.legacyScopeName = params.legacyScopeName
|
||||
if (params.contentRegExp) this.contentRegExp = new RegExp(params.contentRegExp)
|
||||
|
||||
this.folds = params.folds || []
|
||||
|
||||
this.commentStrings = {
|
||||
commentStartString: params.comments && params.comments.start,
|
||||
commentEndString: params.comments && params.comments.end
|
||||
}
|
||||
|
||||
const scopeSelectors = {}
|
||||
for (const key in params.scopes || {}) {
|
||||
scopeSelectors[key] = params.scopes[key]
|
||||
.split('.')
|
||||
.map(s => `syntax--${s}`)
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
this.scopeMap = new SyntaxScopeMap(scopeSelectors)
|
||||
this.fileTypes = params.fileTypes
|
||||
|
||||
// TODO - When we upgrade to a new enough version of node, use `require.resolve`
|
||||
// with the new `paths` option instead of this private API.
|
||||
const languageModulePath = Module._resolveFilename(params.parser, {
|
||||
id: filePath,
|
||||
filename: filePath,
|
||||
paths: Module._nodeModulePaths(path.dirname(filePath))
|
||||
})
|
||||
|
||||
this.languageModule = require(languageModulePath)
|
||||
this.scopesById = new Map()
|
||||
this.idsByScope = {}
|
||||
this.nextScopeId = 256 + 1
|
||||
this.registration = null
|
||||
}
|
||||
|
||||
idForScope (scope) {
|
||||
let id = this.idsByScope[scope]
|
||||
if (!id) {
|
||||
id = this.nextScopeId += 2
|
||||
this.idsByScope[scope] = id
|
||||
this.scopesById.set(id, scope)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
classNameForScopeId (id) {
|
||||
return this.scopesById.get(id)
|
||||
}
|
||||
|
||||
get scopeName () {
|
||||
return this.id
|
||||
}
|
||||
|
||||
activate () {
|
||||
this.registration = this.registry.addGrammar(this)
|
||||
}
|
||||
|
||||
deactivate () {
|
||||
if (this.registration) this.registration.dispose()
|
||||
}
|
||||
}
|
||||
527
src/tree-sitter-language-mode.js
Normal file
527
src/tree-sitter-language-mode.js
Normal file
@@ -0,0 +1,527 @@
|
||||
const {Document} = require('tree-sitter')
|
||||
const {Point, Range, Emitter} = require('atom')
|
||||
const ScopeDescriptor = require('./scope-descriptor')
|
||||
const TokenizedLine = require('./tokenized-line')
|
||||
const TextMateLanguageMode = require('./text-mate-language-mode')
|
||||
|
||||
let nextId = 0
|
||||
|
||||
module.exports =
|
||||
class TreeSitterLanguageMode {
|
||||
constructor ({buffer, grammar, config}) {
|
||||
this.id = nextId++
|
||||
this.buffer = buffer
|
||||
this.grammar = grammar
|
||||
this.config = config
|
||||
this.document = new Document()
|
||||
this.document.setInput(new TreeSitterTextBufferInput(buffer))
|
||||
this.document.setLanguage(grammar.languageModule)
|
||||
this.document.parse()
|
||||
this.rootScopeDescriptor = new ScopeDescriptor({scopes: [this.grammar.id]})
|
||||
this.emitter = new Emitter()
|
||||
this.isFoldableCache = []
|
||||
|
||||
// TODO: Remove this once TreeSitterLanguageMode implements its own auto-indentation system. This
|
||||
// is temporarily needed in order to delegate to the TextMateLanguageMode's auto-indent system.
|
||||
this.regexesByPattern = {}
|
||||
}
|
||||
|
||||
getLanguageId () {
|
||||
return this.grammar.id
|
||||
}
|
||||
|
||||
bufferDidChange ({oldRange, newRange, oldText, newText}) {
|
||||
const startRow = oldRange.start.row
|
||||
const oldEndRow = oldRange.end.row
|
||||
const newEndRow = newRange.end.row
|
||||
this.isFoldableCache.splice(startRow, oldEndRow - startRow, ...new Array(newEndRow - startRow))
|
||||
this.document.edit({
|
||||
startIndex: this.buffer.characterIndexForPosition(oldRange.start),
|
||||
lengthRemoved: oldText.length,
|
||||
lengthAdded: newText.length,
|
||||
startPosition: oldRange.start,
|
||||
extentRemoved: oldRange.getExtent(),
|
||||
extentAdded: newRange.getExtent()
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
Section - Highlighting
|
||||
*/
|
||||
|
||||
buildHighlightIterator () {
|
||||
const invalidatedRanges = this.document.parse()
|
||||
for (let i = 0, n = invalidatedRanges.length; i < n; i++) {
|
||||
const range = invalidatedRanges[i]
|
||||
const startRow = range.start.row
|
||||
const endRow = range.end.row
|
||||
for (let row = startRow; row < endRow; row++) {
|
||||
this.isFoldableCache[row] = undefined
|
||||
}
|
||||
this.emitter.emit('did-change-highlighting', range)
|
||||
}
|
||||
return new TreeSitterHighlightIterator(this)
|
||||
}
|
||||
|
||||
onDidChangeHighlighting (callback) {
|
||||
return this.emitter.on('did-change-hightlighting', callback)
|
||||
}
|
||||
|
||||
classNameForScopeId (scopeId) {
|
||||
return this.grammar.classNameForScopeId(scopeId)
|
||||
}
|
||||
|
||||
/*
|
||||
Section - Commenting
|
||||
*/
|
||||
|
||||
commentStringsForPosition () {
|
||||
return this.grammar.commentStrings
|
||||
}
|
||||
|
||||
isRowCommented () {
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
Section - Indentation
|
||||
*/
|
||||
|
||||
suggestedIndentForLineAtBufferRow (row, line, tabLength) {
|
||||
return this._suggestedIndentForLineWithScopeAtBufferRow(
|
||||
row,
|
||||
line,
|
||||
this.rootScopeDescriptor,
|
||||
tabLength
|
||||
)
|
||||
}
|
||||
|
||||
suggestedIndentForBufferRow (row, tabLength, options) {
|
||||
return this._suggestedIndentForLineWithScopeAtBufferRow(
|
||||
row,
|
||||
this.buffer.lineForRow(row),
|
||||
this.rootScopeDescriptor,
|
||||
tabLength,
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
indentLevelForLine (line, tabLength = tabLength) {
|
||||
let indentLength = 0
|
||||
for (let i = 0, {length} = line; i < length; i++) {
|
||||
const char = line[i]
|
||||
if (char === '\t') {
|
||||
indentLength += tabLength - (indentLength % tabLength)
|
||||
} else if (char === ' ') {
|
||||
indentLength++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return indentLength / tabLength
|
||||
}
|
||||
|
||||
/*
|
||||
Section - Folding
|
||||
*/
|
||||
|
||||
isFoldableAtRow (row) {
|
||||
if (this.isFoldableCache[row] != null) return this.isFoldableCache[row]
|
||||
const result = this.getFoldableRangeContainingPoint(Point(row, Infinity), 0, true) != null
|
||||
this.isFoldableCache[row] = result
|
||||
return result
|
||||
}
|
||||
|
||||
getFoldableRanges () {
|
||||
return this.getFoldableRangesAtIndentLevel(null)
|
||||
}
|
||||
|
||||
getFoldableRangesAtIndentLevel (goalLevel) {
|
||||
let result = []
|
||||
let stack = [{node: this.document.rootNode, level: 0}]
|
||||
while (stack.length > 0) {
|
||||
const {node, level} = stack.pop()
|
||||
|
||||
const range = this.getFoldableRangeForNode(node)
|
||||
if (range) {
|
||||
if (goalLevel == null || level === goalLevel) {
|
||||
let updatedExistingRange = false
|
||||
for (let i = 0, {length} = result; i < length; i++) {
|
||||
if (result[i].start.row === range.start.row &&
|
||||
result[i].end.row === range.end.row) {
|
||||
result[i] = range
|
||||
updatedExistingRange = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!updatedExistingRange) result.push(range)
|
||||
}
|
||||
}
|
||||
|
||||
const parentStartRow = node.startPosition.row
|
||||
const parentEndRow = node.endPosition.row
|
||||
for (let children = node.namedChildren, i = 0, {length} = children; i < length; i++) {
|
||||
const child = children[i]
|
||||
const {startPosition: childStart, endPosition: childEnd} = child
|
||||
if (childEnd.row > childStart.row) {
|
||||
if (childStart.row === parentStartRow && childEnd.row === parentEndRow) {
|
||||
stack.push({node: child, level: level})
|
||||
} else {
|
||||
const childLevel = range && range.containsPoint(childStart) && range.containsPoint(childEnd)
|
||||
? level + 1
|
||||
: level
|
||||
if (childLevel <= goalLevel || goalLevel == null) {
|
||||
stack.push({node: child, level: childLevel})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.sort((a, b) => a.start.row - b.start.row)
|
||||
}
|
||||
|
||||
getFoldableRangeContainingPoint (point, tabLength, existenceOnly = false) {
|
||||
let node = this.document.rootNode.descendantForPosition(this.buffer.clipPosition(point))
|
||||
while (node) {
|
||||
if (existenceOnly && node.startPosition.row < point.row) break
|
||||
if (node.endPosition.row > point.row) {
|
||||
const range = this.getFoldableRangeForNode(node, existenceOnly)
|
||||
if (range) return range
|
||||
}
|
||||
node = node.parent
|
||||
}
|
||||
}
|
||||
|
||||
getFoldableRangeForNode (node, existenceOnly) {
|
||||
const {children, type: nodeType} = node
|
||||
const childCount = children.length
|
||||
let childTypes
|
||||
|
||||
for (var i = 0, {length} = this.grammar.folds; i < length; i++) {
|
||||
const foldEntry = this.grammar.folds[i]
|
||||
|
||||
if (foldEntry.type) {
|
||||
if (typeof foldEntry.type === 'string') {
|
||||
if (foldEntry.type !== nodeType) continue
|
||||
} else {
|
||||
if (!foldEntry.type.includes(nodeType)) continue
|
||||
}
|
||||
}
|
||||
|
||||
let foldStart
|
||||
const startEntry = foldEntry.start
|
||||
if (startEntry) {
|
||||
if (startEntry.index != null) {
|
||||
const child = children[startEntry.index]
|
||||
if (!child || (startEntry.type && startEntry.type !== child.type)) continue
|
||||
foldStart = child.endPosition
|
||||
} else {
|
||||
if (!childTypes) childTypes = children.map(child => child.type)
|
||||
const index = typeof startEntry.type === 'string'
|
||||
? childTypes.indexOf(startEntry.type)
|
||||
: childTypes.findIndex(type => startEntry.type.includes(type))
|
||||
if (index === -1) continue
|
||||
foldStart = children[index].endPosition
|
||||
}
|
||||
} else {
|
||||
foldStart = new Point(node.startPosition.row, Infinity)
|
||||
}
|
||||
|
||||
let foldEnd
|
||||
const endEntry = foldEntry.end
|
||||
if (endEntry) {
|
||||
let foldEndNode
|
||||
if (endEntry.index != null) {
|
||||
const index = endEntry.index < 0 ? childCount + endEntry.index : endEntry.index
|
||||
foldEndNode = children[index]
|
||||
if (!foldEndNode || (endEntry.type && endEntry.type !== foldEndNode.type)) continue
|
||||
} else {
|
||||
if (!childTypes) childTypes = children.map(foldEndNode => foldEndNode.type)
|
||||
const index = typeof endEntry.type === 'string'
|
||||
? childTypes.indexOf(endEntry.type)
|
||||
: childTypes.findIndex(type => endEntry.type.includes(type))
|
||||
if (index === -1) continue
|
||||
foldEndNode = children[index]
|
||||
}
|
||||
|
||||
if (foldEndNode.endIndex - foldEndNode.startIndex > 1 && foldEndNode.startPosition.row > foldStart.row) {
|
||||
foldEnd = new Point(foldEndNode.startPosition.row - 1, Infinity)
|
||||
} else {
|
||||
foldEnd = foldEndNode.startPosition
|
||||
}
|
||||
} else {
|
||||
const {endPosition} = node
|
||||
if (endPosition.column === 0) {
|
||||
foldEnd = Point(endPosition.row - 1, Infinity)
|
||||
} else if (childCount > 0) {
|
||||
foldEnd = endPosition
|
||||
} else {
|
||||
foldEnd = Point(endPosition.row, 0)
|
||||
}
|
||||
}
|
||||
|
||||
return existenceOnly ? true : new Range(foldStart, foldEnd)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Syntax Tree APIs
|
||||
*/
|
||||
|
||||
getRangeForSyntaxNodeContainingRange (range) {
|
||||
const startIndex = this.buffer.characterIndexForPosition(range.start)
|
||||
const endIndex = this.buffer.characterIndexForPosition(range.end)
|
||||
let node = this.document.rootNode.descendantForIndex(startIndex, endIndex - 1)
|
||||
while (node && node.startIndex === startIndex && node.endIndex === endIndex) {
|
||||
node = node.parent
|
||||
}
|
||||
if (node) return new Range(node.startPosition, node.endPosition)
|
||||
}
|
||||
|
||||
/*
|
||||
Section - Backward compatibility shims
|
||||
*/
|
||||
|
||||
tokenizedLineForRow (row) {
|
||||
return new TokenizedLine({
|
||||
openScopes: [],
|
||||
text: this.buffer.lineForRow(row),
|
||||
tags: [],
|
||||
ruleStack: [],
|
||||
lineEnding: this.buffer.lineEndingForRow(row),
|
||||
tokenIterator: null,
|
||||
grammar: this.grammar
|
||||
})
|
||||
}
|
||||
|
||||
scopeDescriptorForPosition (point) {
|
||||
const result = []
|
||||
let node = this.document.rootNode.descendantForPosition(point)
|
||||
|
||||
// Don't include anonymous token types like '(' because they prevent scope chains
|
||||
// from being parsed as CSS selectors by the `slick` parser. Other css selector
|
||||
// parsers like `postcss-selector-parser` do allow arbitrary quoted strings in
|
||||
// selectors.
|
||||
if (!node.isNamed) node = node.parent
|
||||
|
||||
while (node) {
|
||||
result.push(node.type)
|
||||
node = node.parent
|
||||
}
|
||||
result.push(this.grammar.id)
|
||||
return new ScopeDescriptor({scopes: result.reverse()})
|
||||
}
|
||||
|
||||
hasTokenForSelector (scopeSelector) {
|
||||
return false
|
||||
}
|
||||
|
||||
getGrammar () {
|
||||
return this.grammar
|
||||
}
|
||||
}
|
||||
|
||||
class TreeSitterHighlightIterator {
|
||||
constructor (layer, document) {
|
||||
this.layer = layer
|
||||
|
||||
// Conceptually, the iterator represents a single position in the text. It stores this
|
||||
// position both as a character index and as a `Point`. This position corresponds to a
|
||||
// leaf node of the syntax tree, which either contains or follows the iterator's
|
||||
// textual position. The `currentNode` property represents that leaf node, and
|
||||
// `currentChildIndex` represents the child index of that leaf node within its parent.
|
||||
this.currentIndex = null
|
||||
this.currentPosition = null
|
||||
this.currentNode = null
|
||||
this.currentChildIndex = null
|
||||
|
||||
// In order to determine which selectors match its current node, the iterator maintains
|
||||
// a list of the current node's ancestors. Because the selectors can use the `:nth-child`
|
||||
// pseudo-class, each node's child index is also stored.
|
||||
this.containingNodeTypes = []
|
||||
this.containingNodeChildIndices = []
|
||||
|
||||
// At any given position, the iterator exposes the list of class names that should be
|
||||
// *ended* at its current position and the list of class names that should be *started*
|
||||
// at its current position.
|
||||
this.closeTags = []
|
||||
this.openTags = []
|
||||
}
|
||||
|
||||
seek (targetPosition) {
|
||||
const containingTags = []
|
||||
|
||||
this.closeTags.length = 0
|
||||
this.openTags.length = 0
|
||||
this.containingNodeTypes.length = 0
|
||||
this.containingNodeChildIndices.length = 0
|
||||
this.currentPosition = targetPosition
|
||||
this.currentIndex = this.layer.buffer.characterIndexForPosition(targetPosition)
|
||||
|
||||
var node = this.layer.document.rootNode
|
||||
var childIndex = -1
|
||||
var nodeContainsTarget = true
|
||||
for (;;) {
|
||||
this.currentNode = node
|
||||
this.currentChildIndex = childIndex
|
||||
if (!nodeContainsTarget) break
|
||||
this.containingNodeTypes.push(node.type)
|
||||
this.containingNodeChildIndices.push(childIndex)
|
||||
|
||||
const scopeName = this.currentScopeName()
|
||||
if (scopeName) {
|
||||
const id = this.layer.grammar.idForScope(scopeName)
|
||||
if (this.currentIndex === node.startIndex) {
|
||||
this.openTags.push(id)
|
||||
} else {
|
||||
containingTags.push(id)
|
||||
}
|
||||
}
|
||||
|
||||
node = node.firstChildForIndex(this.currentIndex)
|
||||
if (node) {
|
||||
if (node.startIndex > this.currentIndex) nodeContainsTarget = false
|
||||
childIndex = node.childIndex
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return containingTags
|
||||
}
|
||||
|
||||
moveToSuccessor () {
|
||||
this.closeTags.length = 0
|
||||
this.openTags.length = 0
|
||||
|
||||
if (!this.currentNode) {
|
||||
this.currentPosition = {row: Infinity, column: Infinity}
|
||||
return false
|
||||
}
|
||||
|
||||
do {
|
||||
if (this.currentIndex < this.currentNode.startIndex) {
|
||||
this.currentIndex = this.currentNode.startIndex
|
||||
this.currentPosition = this.currentNode.startPosition
|
||||
this.pushOpenTag()
|
||||
this.descendLeft()
|
||||
} else if (this.currentIndex < this.currentNode.endIndex) {
|
||||
while (true) {
|
||||
this.currentIndex = this.currentNode.endIndex
|
||||
this.currentPosition = this.currentNode.endPosition
|
||||
this.pushCloseTag()
|
||||
|
||||
const {nextSibling} = this.currentNode
|
||||
if (nextSibling) {
|
||||
this.currentNode = nextSibling
|
||||
this.currentChildIndex++
|
||||
if (this.currentIndex === nextSibling.startIndex) {
|
||||
this.pushOpenTag()
|
||||
this.descendLeft()
|
||||
}
|
||||
break
|
||||
} else {
|
||||
this.currentNode = this.currentNode.parent
|
||||
this.currentChildIndex = last(this.containingNodeChildIndices)
|
||||
if (!this.currentNode) break
|
||||
}
|
||||
}
|
||||
} else if (this.currentNode.startIndex < this.currentNode.endIndex) {
|
||||
this.currentNode = this.currentNode.nextSibling
|
||||
if (this.currentNode) {
|
||||
this.currentChildIndex++
|
||||
this.currentPosition = this.currentNode.startPosition
|
||||
this.currentIndex = this.currentNode.startIndex
|
||||
this.pushOpenTag()
|
||||
this.descendLeft()
|
||||
}
|
||||
} else {
|
||||
this.pushCloseTag()
|
||||
this.currentNode = this.currentNode.parent
|
||||
this.currentChildIndex = last(this.containingNodeChildIndices)
|
||||
}
|
||||
} while (this.closeTags.length === 0 && this.openTags.length === 0 && this.currentNode)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
getPosition () {
|
||||
return this.currentPosition
|
||||
}
|
||||
|
||||
getCloseScopeIds () {
|
||||
return this.closeTags.slice()
|
||||
}
|
||||
|
||||
getOpenScopeIds () {
|
||||
return this.openTags.slice()
|
||||
}
|
||||
|
||||
// Private methods
|
||||
|
||||
descendLeft () {
|
||||
let child
|
||||
while ((child = this.currentNode.firstChild) && this.currentIndex === child.startIndex) {
|
||||
this.currentNode = child
|
||||
this.currentChildIndex = 0
|
||||
this.pushOpenTag()
|
||||
}
|
||||
}
|
||||
|
||||
currentScopeName () {
|
||||
return this.layer.grammar.scopeMap.get(
|
||||
this.containingNodeTypes,
|
||||
this.containingNodeChildIndices,
|
||||
this.currentNode.isNamed
|
||||
)
|
||||
}
|
||||
|
||||
pushCloseTag () {
|
||||
const scopeName = this.currentScopeName()
|
||||
if (scopeName) this.closeTags.push(this.layer.grammar.idForScope(scopeName))
|
||||
this.containingNodeTypes.pop()
|
||||
this.containingNodeChildIndices.pop()
|
||||
}
|
||||
|
||||
pushOpenTag () {
|
||||
this.containingNodeTypes.push(this.currentNode.type)
|
||||
this.containingNodeChildIndices.push(this.currentChildIndex)
|
||||
const scopeName = this.currentScopeName()
|
||||
if (scopeName) this.openTags.push(this.layer.grammar.idForScope(scopeName))
|
||||
}
|
||||
}
|
||||
|
||||
class TreeSitterTextBufferInput {
|
||||
constructor (buffer) {
|
||||
this.buffer = buffer
|
||||
this.seek(0)
|
||||
}
|
||||
|
||||
seek (characterIndex) {
|
||||
this.position = this.buffer.positionForCharacterIndex(characterIndex)
|
||||
}
|
||||
|
||||
read () {
|
||||
const endPosition = this.buffer.clipPosition(this.position.traverse({row: 1000, column: 0}))
|
||||
const text = this.buffer.getTextInRange([this.position, endPosition])
|
||||
this.position = endPosition
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
function last (array) {
|
||||
return array[array.length - 1]
|
||||
}
|
||||
|
||||
// TODO: Remove this once TreeSitterLanguageMode implements its own auto-indent system.
|
||||
[
|
||||
'_suggestedIndentForLineWithScopeAtBufferRow',
|
||||
'suggestedIndentForEditedBufferRow',
|
||||
'increaseIndentRegexForScopeDescriptor',
|
||||
'decreaseIndentRegexForScopeDescriptor',
|
||||
'decreaseNextIndentRegexForScopeDescriptor',
|
||||
'regexForPattern'
|
||||
].forEach(methodName => {
|
||||
module.exports.prototype[methodName] = TextMateLanguageMode.prototype[methodName]
|
||||
})
|
||||
@@ -9,6 +9,7 @@ class WindowEventHandler {
|
||||
this.handleFocusNext = this.handleFocusNext.bind(this)
|
||||
this.handleFocusPrevious = this.handleFocusPrevious.bind(this)
|
||||
this.handleWindowBlur = this.handleWindowBlur.bind(this)
|
||||
this.handleWindowResize = this.handleWindowResize.bind(this)
|
||||
this.handleEnterFullScreen = this.handleEnterFullScreen.bind(this)
|
||||
this.handleLeaveFullScreen = this.handleLeaveFullScreen.bind(this)
|
||||
this.handleWindowBeforeunload = this.handleWindowBeforeunload.bind(this)
|
||||
@@ -51,6 +52,7 @@ class WindowEventHandler {
|
||||
this.addEventListener(this.window, 'beforeunload', this.handleWindowBeforeunload)
|
||||
this.addEventListener(this.window, 'focus', this.handleWindowFocus)
|
||||
this.addEventListener(this.window, 'blur', this.handleWindowBlur)
|
||||
this.addEventListener(this.window, 'resize', this.handleWindowResize)
|
||||
|
||||
this.addEventListener(this.document, 'keyup', this.handleDocumentKeyEvent)
|
||||
this.addEventListener(this.document, 'keydown', this.handleDocumentKeyEvent)
|
||||
@@ -189,6 +191,10 @@ class WindowEventHandler {
|
||||
this.atomEnvironment.storeWindowDimensions()
|
||||
}
|
||||
|
||||
handleWindowResize () {
|
||||
this.atomEnvironment.storeWindowDimensions()
|
||||
}
|
||||
|
||||
handleEnterFullScreen () {
|
||||
this.document.body.classList.add('fullscreen')
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ class WorkspaceElement extends HTMLElement {
|
||||
|
||||
this.addEventListener('mousewheel', this.handleMousewheel.bind(this), true)
|
||||
window.addEventListener('dragstart', this.handleDragStart)
|
||||
window.addEventListener('mousemove', this.handleEdgesMouseMove)
|
||||
|
||||
this.panelContainers = {
|
||||
top: this.model.panelContainers.top.getElement(),
|
||||
@@ -132,6 +133,10 @@ class WorkspaceElement extends HTMLElement {
|
||||
return this
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.subscriptions.dispose()
|
||||
}
|
||||
|
||||
getModel () { return this.model }
|
||||
|
||||
handleDragStart (event) {
|
||||
@@ -169,7 +174,6 @@ class WorkspaceElement extends HTMLElement {
|
||||
// being hovered.
|
||||
this.cursorInCenter = false
|
||||
this.updateHoveredDock({x: event.pageX, y: event.pageY})
|
||||
window.addEventListener('mousemove', this.handleEdgesMouseMove)
|
||||
window.addEventListener('dragend', this.handleDockDragEnd)
|
||||
}
|
||||
|
||||
@@ -199,7 +203,6 @@ class WorkspaceElement extends HTMLElement {
|
||||
|
||||
checkCleanupDockHoverEvents () {
|
||||
if (this.cursorInCenter && !this.hoveredDock) {
|
||||
window.removeEventListener('mousemove', this.handleEdgesMouseMove)
|
||||
window.removeEventListener('dragend', this.handleDockDragEnd)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,7 +310,10 @@ module.exports = class Workspace extends Model {
|
||||
this.originalFontSize = null
|
||||
this.openers = []
|
||||
this.destroyedItemURIs = []
|
||||
this.element = null
|
||||
if (this.element) {
|
||||
this.element.destroy()
|
||||
this.element = null
|
||||
}
|
||||
this.consumeServices(this.packageManager)
|
||||
}
|
||||
|
||||
@@ -494,10 +497,12 @@ module.exports = class Workspace extends Model {
|
||||
if (item instanceof TextEditor) {
|
||||
const subscriptions = new CompositeDisposable(
|
||||
this.textEditorRegistry.add(item),
|
||||
this.textEditorRegistry.maintainGrammar(item),
|
||||
this.textEditorRegistry.maintainConfig(item),
|
||||
item.observeGrammar(this.handleGrammarUsed.bind(this))
|
||||
)
|
||||
if (!this.project.findBufferForId(item.buffer.id)) {
|
||||
this.project.addBuffer(item.buffer)
|
||||
}
|
||||
item.onDidDestroy(() => { subscriptions.dispose() })
|
||||
this.emitter.emit('did-add-text-editor', {textEditor: item, pane, index})
|
||||
}
|
||||
@@ -1158,16 +1163,17 @@ module.exports = class Workspace extends Model {
|
||||
// * `uri` A {String} containing a URI.
|
||||
//
|
||||
// Returns a {Promise} that resolves to the {TextEditor} (or other item) for the given URI.
|
||||
createItemForURI (uri, options) {
|
||||
async createItemForURI (uri, options) {
|
||||
if (uri != null) {
|
||||
for (let opener of this.getOpeners()) {
|
||||
for (const opener of this.getOpeners()) {
|
||||
const item = opener(uri, options)
|
||||
if (item != null) return Promise.resolve(item)
|
||||
if (item != null) return item
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return this.openTextFile(uri, options)
|
||||
const item = await this.openTextFile(uri, options)
|
||||
return item
|
||||
} catch (error) {
|
||||
switch (error.code) {
|
||||
case 'CANCELLED':
|
||||
@@ -1197,7 +1203,7 @@ module.exports = class Workspace extends Model {
|
||||
}
|
||||
}
|
||||
|
||||
openTextFile (uri, options) {
|
||||
async openTextFile (uri, options) {
|
||||
const filePath = this.project.resolvePath(uri)
|
||||
|
||||
if (filePath != null) {
|
||||
@@ -1213,24 +1219,37 @@ module.exports = class Workspace extends Model {
|
||||
|
||||
const fileSize = fs.getSizeSync(filePath)
|
||||
|
||||
const largeFileMode = fileSize >= (2 * 1048576) // 2MB
|
||||
if (fileSize >= (this.config.get('core.warnOnLargeFileLimit') * 1048576)) { // 20MB by default
|
||||
const choice = this.applicationDelegate.confirm({
|
||||
let [resolveConfirmFileOpenPromise, rejectConfirmFileOpenPromise] = []
|
||||
const confirmFileOpenPromise = new Promise((resolve, reject) => {
|
||||
resolveConfirmFileOpenPromise = resolve
|
||||
rejectConfirmFileOpenPromise = reject
|
||||
})
|
||||
|
||||
if (fileSize >= (this.config.get('core.warnOnLargeFileLimit') * 1048576)) { // 40MB by default
|
||||
this.applicationDelegate.confirm({
|
||||
message: 'Atom will be unresponsive during the loading of very large files.',
|
||||
detailedMessage: 'Do you still want to load this file?',
|
||||
detail: 'Do you still want to load this file?',
|
||||
buttons: ['Proceed', 'Cancel']
|
||||
}, response => {
|
||||
if (response === 1) {
|
||||
rejectConfirmFileOpenPromise()
|
||||
} else {
|
||||
resolveConfirmFileOpenPromise()
|
||||
}
|
||||
})
|
||||
if (choice === 1) {
|
||||
const error = new Error()
|
||||
error.code = 'CANCELLED'
|
||||
throw error
|
||||
}
|
||||
} else {
|
||||
resolveConfirmFileOpenPromise()
|
||||
}
|
||||
|
||||
return this.project.bufferForPath(filePath, options)
|
||||
.then(buffer => {
|
||||
return this.textEditorRegistry.build(Object.assign({buffer, largeFileMode, autoHeight: false}, options))
|
||||
})
|
||||
try {
|
||||
await confirmFileOpenPromise
|
||||
const buffer = await this.project.bufferForPath(filePath, options)
|
||||
return this.textEditorRegistry.build(Object.assign({buffer, autoHeight: false}, options))
|
||||
} catch (e) {
|
||||
const error = new Error()
|
||||
error.code = 'CANCELLED'
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
handleGrammarUsed (grammar) {
|
||||
@@ -1250,11 +1269,8 @@ module.exports = class Workspace extends Model {
|
||||
// Returns a {TextEditor}.
|
||||
buildTextEditor (params) {
|
||||
const editor = this.textEditorRegistry.build(params)
|
||||
const subscriptions = new CompositeDisposable(
|
||||
this.textEditorRegistry.maintainGrammar(editor),
|
||||
this.textEditorRegistry.maintainConfig(editor)
|
||||
)
|
||||
editor.onDidDestroy(() => { subscriptions.dispose() })
|
||||
const subscription = this.textEditorRegistry.maintainConfig(editor)
|
||||
editor.onDidDestroy(() => subscription.dispose())
|
||||
return editor
|
||||
}
|
||||
|
||||
@@ -1557,6 +1573,7 @@ module.exports = class Workspace extends Model {
|
||||
if (this.activeItemSubscriptions != null) {
|
||||
this.activeItemSubscriptions.dispose()
|
||||
}
|
||||
if (this.element) this.element.destroy()
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1990,25 +2007,22 @@ module.exports = class Workspace extends Model {
|
||||
|
||||
checkoutHeadRevision (editor) {
|
||||
if (editor.getPath()) {
|
||||
const checkoutHead = () => {
|
||||
return this.project.repositoryForDirectory(new Directory(editor.getDirectoryPath()))
|
||||
.then(repository => repository && repository.checkoutHeadForEditor(editor))
|
||||
const checkoutHead = async () => {
|
||||
const repository = await this.project.repositoryForDirectory(new Directory(editor.getDirectoryPath()))
|
||||
if (repository) repository.checkoutHeadForEditor(editor)
|
||||
}
|
||||
|
||||
if (this.config.get('editor.confirmCheckoutHeadRevision')) {
|
||||
this.applicationDelegate.confirm({
|
||||
message: 'Confirm Checkout HEAD Revision',
|
||||
detailedMessage: `Are you sure you want to discard all changes to "${editor.getFileName()}" since the last Git commit?`,
|
||||
buttons: {
|
||||
OK: checkoutHead,
|
||||
Cancel: null
|
||||
}
|
||||
detail: `Are you sure you want to discard all changes to "${editor.getFileName()}" since the last Git commit?`,
|
||||
buttons: ['OK', 'Cancel']
|
||||
}, response => {
|
||||
if (response === 0) checkoutHead()
|
||||
})
|
||||
} else {
|
||||
return checkoutHead()
|
||||
checkoutHead()
|
||||
}
|
||||
} else {
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user