Merge branch 'master' of https://github.com/atom/atom into b3-failing-seed

This commit is contained in:
Linus Eriksson
2018-01-19 19:08:21 +01:00
116 changed files with 9928 additions and 6823 deletions

View File

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

View File

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

View File

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

View File

@@ -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.'
}, () => {})
})
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,225 @@
const {app, Menu} = require('electron')
const _ = require('underscore-plus')
const MenuHelpers = require('../menu-helpers')
// Used to manage the global application menu.
//
// It's created by {AtomApplication} upon instantiation and used to add, remove
// and maintain the state of all menu items.
module.exports =
class ApplicationMenu {
constructor (version, autoUpdateManager) {
this.version = version
this.autoUpdateManager = autoUpdateManager
this.windowTemplates = new WeakMap()
this.setActiveTemplate(this.getDefaultTemplate())
this.autoUpdateManager.on('state-changed', state => this.showUpdateMenuItem(state))
}
// Public: Updates the entire menu with the given keybindings.
//
// window - The BrowserWindow this menu template is associated with.
// template - The Object which describes the menu to display.
// keystrokesByCommand - An Object where the keys are commands and the values
// are Arrays containing the keystroke.
update (window, template, keystrokesByCommand) {
this.translateTemplate(template, keystrokesByCommand)
this.substituteVersion(template)
this.windowTemplates.set(window, template)
if (window === this.lastFocusedWindow) return this.setActiveTemplate(template)
}
setActiveTemplate (template) {
if (!_.isEqual(template, this.activeTemplate)) {
this.activeTemplate = template
this.menu = Menu.buildFromTemplate(_.deepClone(template))
Menu.setApplicationMenu(this.menu)
}
return this.showUpdateMenuItem(this.autoUpdateManager.getState())
}
// Register a BrowserWindow with this application menu.
addWindow (window) {
if (this.lastFocusedWindow == null) this.lastFocusedWindow = window
const focusHandler = () => {
this.lastFocusedWindow = window
const template = this.windowTemplates.get(window)
if (template) this.setActiveTemplate(template)
}
window.on('focus', focusHandler)
window.once('closed', () => {
if (window === this.lastFocusedWindow) this.lastFocusedWindow = null
this.windowTemplates.delete(window)
window.removeListener('focus', focusHandler)
})
this.enableWindowSpecificItems(true)
}
// Flattens the given menu and submenu items into an single Array.
//
// menu - A complete menu configuration object for atom-shell's menu API.
//
// Returns an Array of native menu items.
flattenMenuItems (menu) {
const object = menu.items || {}
let items = []
for (let index in object) {
const item = object[index]
items.push(item)
if (item.submenu) items = items.concat(this.flattenMenuItems(item.submenu))
}
return items
}
// Flattens the given menu template into an single Array.
//
// template - An object describing the menu item.
//
// Returns an Array of native menu items.
flattenMenuTemplate (template) {
let items = []
for (let item of template) {
items.push(item)
if (item.submenu) items = items.concat(this.flattenMenuTemplate(item.submenu))
}
return items
}
// Public: Used to make all window related menu items are active.
//
// enable - If true enables all window specific items, if false disables all
// window specific items.
enableWindowSpecificItems (enable) {
for (let item of this.flattenMenuItems(this.menu)) {
if (item.metadata && item.metadata.windowSpecific) item.enabled = enable
}
}
// Replaces VERSION with the current version.
substituteVersion (template) {
let item = this.flattenMenuTemplate(template).find(({label}) => label === 'VERSION')
if (item) item.label = `Version ${this.version}`
}
// Sets the proper visible state the update menu items
showUpdateMenuItem (state) {
const items = this.flattenMenuItems(this.menu)
const checkForUpdateItem = items.find(({label}) => label === 'Check for Update')
const checkingForUpdateItem = items.find(({label}) => label === 'Checking for Update')
const downloadingUpdateItem = items.find(({label}) => label === 'Downloading Update')
const installUpdateItem = items.find(({label}) => label === 'Restart and Install Update')
if (!checkForUpdateItem || !checkingForUpdateItem ||
!downloadingUpdateItem || !installUpdateItem) return
checkForUpdateItem.visible = false
checkingForUpdateItem.visible = false
downloadingUpdateItem.visible = false
installUpdateItem.visible = false
switch (state) {
case 'idle':
case 'error':
case 'no-update-available':
checkForUpdateItem.visible = true
break
case 'checking':
checkingForUpdateItem.visible = true
break
case 'downloading':
downloadingUpdateItem.visible = true
break
case 'update-available':
installUpdateItem.visible = true
break
}
}
// Default list of menu items.
//
// Returns an Array of menu item Objects.
getDefaultTemplate () {
return [{
label: 'Atom',
submenu: [
{
label: 'Check for Update',
metadata: {autoUpdate: true}
},
{
label: 'Reload',
accelerator: 'Command+R',
click: () => {
const window = this.focusedWindow()
if (window) window.reload()
}
},
{
label: 'Close Window',
accelerator: 'Command+Shift+W',
click: () => {
const window = this.focusedWindow()
if (window) window.close()
}
},
{
label: 'Toggle Dev Tools',
accelerator: 'Command+Alt+I',
click: () => {
const window = this.focusedWindow()
if (window) window.toggleDevTools()
}
},
{
label: 'Quit',
accelerator: 'Command+Q',
click: () => app.quit()
}
]
}]
}
focusedWindow () {
return global.atomApplication.getAllWindows().find(window => window.isFocused())
}
// Combines a menu template with the appropriate keystroke.
//
// template - An Object conforming to atom-shell's menu api but lacking
// accelerator and click properties.
// keystrokesByCommand - An Object where the keys are commands and the values
// are Arrays containing the keystroke.
//
// Returns a complete menu configuration object for atom-shell's menu API.
translateTemplate (template, keystrokesByCommand) {
template.forEach(item => {
if (item.metadata == null) item.metadata = {}
if (item.command) {
item.accelerator = this.acceleratorForCommand(item.command, keystrokesByCommand)
item.click = () => global.atomApplication.sendCommand(item.command, item.commandDetail)
if (!/^application:/.test(item.command, item.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)
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: [
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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