Files
atom/src/package.coffee
Nathan Sobo f10076c87d Prevent activation events from bubbling
The react editor is wrapped in another div with the class of .editor for
backward compatibility. This prevents activation events registered on
the .editor selector from being triggered twice.
2014-04-22 17:10:21 -06:00

323 lines
10 KiB
CoffeeScript

path = require 'path'
_ = require 'underscore-plus'
async = require 'async'
CSON = require 'season'
fs = require 'fs-plus'
{Emitter} = require 'emissary'
Q = require 'q'
$ = null # Defer require in case this is in the window-less browser process
ScopedProperties = require './scoped-properties'
# Loads and activates a package's main module and resources such as
# stylesheets, keymaps, grammar, editor properties, and menus.
module.exports =
class Package
Emitter.includeInto(this)
@stylesheetsDir: 'stylesheets'
@loadMetadata: (packagePath, ignoreErrors=false) ->
if metadataPath = CSON.resolve(path.join(packagePath, 'package'))
try
metadata = CSON.readFileSync(metadataPath)
catch error
throw error unless ignoreErrors
metadata ?= {}
metadata.name = path.basename(packagePath)
metadata
keymaps: null
menus: null
stylesheets: null
grammars: null
scopedProperties: null
mainModulePath: null
resolvedMainModulePath: false
mainModule: null
constructor: (@path, @metadata) ->
@metadata ?= Package.loadMetadata(@path)
@name = @metadata?.name ? path.basename(@path)
@reset()
enable: ->
atom.config.removeAtKeyPath('core.disabledPackages', @name)
disable: ->
atom.config.pushAtKeyPath('core.disabledPackages', @name)
isTheme: ->
@metadata?.theme?
measure: (key, fn) ->
startTime = Date.now()
value = fn()
@[key] = Date.now() - startTime
value
getType: -> 'atom'
getStylesheetType: -> 'bundled'
load: ->
@measure 'loadTime', =>
try
@loadKeymaps()
@loadMenus()
@loadStylesheets()
@grammarsPromise = @loadGrammars()
@scopedPropertiesPromise = @loadScopedProperties()
@requireMainModule() unless @hasActivationEvents()
catch error
console.warn "Failed to load package named '#{@name}'", error.stack ? error
this
reset: ->
@stylesheets = []
@keymaps = []
@menus = []
@grammars = []
@scopedProperties = []
activate: ->
unless @activationDeferred?
@activationDeferred = Q.defer()
@measure 'activateTime', =>
@activateResources()
if @hasActivationEvents()
@subscribeToActivationEvents()
else
@activateNow()
Q.all([@grammarsPromise, @scopedPropertiesPromise, @activationDeferred.promise])
activateNow: ->
try
@activateConfig()
@activateStylesheets()
if @requireMainModule()
@mainModule.activate(atom.packages.getPackageState(@name) ? {})
@mainActivated = true
catch e
console.warn "Failed to activate package named '#{@name}'", e.stack
@activationDeferred.resolve()
activateConfig: ->
return if @configActivated
@requireMainModule()
if @mainModule?
atom.config.setDefaults(@name, @mainModule.configDefaults)
@mainModule.activateConfig?()
@configActivated = true
activateStylesheets: ->
return if @stylesheetsActivated
type = @getStylesheetType()
for [stylesheetPath, content] in @stylesheets
atom.themes.applyStylesheet(stylesheetPath, content, type)
@stylesheetsActivated = true
activateResources: ->
atom.keymaps.add(keymapPath, map) for [keymapPath, map] in @keymaps
atom.contextMenu.add(menuPath, map['context-menu']) for [menuPath, map] in @menus
atom.menu.add(map.menu) for [menuPath, map] in @menus when map.menu
grammar.activate() for grammar in @grammars
@grammarsActivated = true
scopedProperties.activate() for scopedProperties in @scopedProperties
@scopedPropertiesActivated = true
loadKeymaps: ->
@keymaps = @getKeymapPaths().map (keymapPath) -> [keymapPath, CSON.readFileSync(keymapPath)]
loadMenus: ->
@menus = @getMenuPaths().map (menuPath) -> [menuPath, CSON.readFileSync(menuPath)]
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, atom.themes.loadStylesheet(stylesheetPath)]
getStylesheetsPath: ->
path.join(@path, @constructor.stylesheetsDir)
getStylesheetPaths: ->
stylesheetDirPath = @getStylesheetsPath()
if @metadata.stylesheetMain
[fs.resolve(@path, @metadata.stylesheetMain)]
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'])
loadGrammars: ->
@grammars = []
loadGrammar = (grammarPath, callback) =>
atom.syntax.readGrammar grammarPath, (error, grammar) =>
if error?
console.warn("Failed to load grammar: #{grammarPath}", error.stack ? error)
else
@grammars.push(grammar)
grammar.activate() if @grammarsActivated
callback()
deferred = Q.defer()
grammarsDirPath = path.join(@path, 'grammars')
fs.list grammarsDirPath, ['json', 'cson'], (error, grammarPaths=[]) ->
async.each grammarPaths, loadGrammar, -> deferred.resolve()
deferred.promise
loadScopedProperties: ->
@scopedProperties = []
loadScopedPropertiesFile = (scopedPropertiesPath, callback) =>
ScopedProperties.load scopedPropertiesPath, (error, scopedProperties) =>
if error?
console.warn("Failed to load scoped properties: #{scopedPropertiesPath}", error.stack ? error)
else
@scopedProperties.push(scopedProperties)
scopedProperties.activate() if @scopedPropertiesActivated
callback()
deferred = Q.defer()
scopedPropertiesDirPath = path.join(@path, 'scoped-properties')
fs.list scopedPropertiesDirPath, ['json', 'cson'], (error, scopedPropertiesPaths=[]) ->
async.each scopedPropertiesPaths, loadScopedPropertiesFile, -> deferred.resolve()
deferred.promise
serialize: ->
if @mainActivated
try
@mainModule?.serialize?()
catch e
console.error "Error serializing package '#{@name}'", e.stack
deactivate: ->
@activationDeferred?.reject()
@activationDeferred = null
@unsubscribeFromActivationEvents()
@deactivateResources()
@deactivateConfig()
@mainModule?.deactivate?() if @mainActivated
@emit('deactivated')
deactivateConfig: ->
@mainModule?.deactivateConfig?()
@configActivated = false
deactivateResources: ->
grammar.deactivate() for grammar in @grammars
scopedProperties.deactivate() for scopedProperties in @scopedProperties
atom.keymaps.remove(keymapPath) for [keymapPath] in @keymaps
atom.themes.removeStylesheet(stylesheetPath) for [stylesheetPath] in @stylesheets
@stylesheetsActivated = false
@grammarsActivated = false
@scopedPropertiesActivated = false
reloadStylesheets: ->
oldSheets = _.clone(@stylesheets)
@loadStylesheets()
atom.themes.removeStylesheet(stylesheetPath) for [stylesheetPath] in oldSheets
@reloadStylesheet(stylesheetPath, content) for [stylesheetPath, content] in @stylesheets
reloadStylesheet: (stylesheetPath, content) ->
atom.themes.applyStylesheet(stylesheetPath, content, @getStylesheetType())
requireMainModule: ->
return @mainModule if @mainModule?
mainModulePath = @getMainModulePath()
@mainModule = require(mainModulePath) if fs.isFileSync(mainModulePath)
getMainModulePath: ->
return @mainModulePath if @resolvedMainModulePath
@resolvedMainModulePath = true
mainModulePath =
if @metadata.main
path.join(@path, @metadata.main)
else
path.join(@path, 'index')
@mainModulePath = fs.resolveExtension(mainModulePath, ["", _.keys(require.extensions)...])
hasActivationEvents: ->
if _.isArray(@metadata.activationEvents)
return @metadata.activationEvents.some (activationEvent) ->
activationEvent?.length > 0
else if _.isString(@metadata.activationEvents)
return @metadata.activationEvents.length > 0
else if _.isObject(@metadata.activationEvents)
for event, selector of @metadata.activationEvents
return true if event.length > 0 and selector.length > 0
false
subscribeToActivationEvents: ->
return unless @metadata.activationEvents?
if _.isArray(@metadata.activationEvents)
atom.workspaceView.command(event, @handleActivationEvent) for event in @metadata.activationEvents
else if _.isString(@metadata.activationEvents)
atom.workspaceView.command(@metadata.activationEvents, @handleActivationEvent)
else
atom.workspaceView.command(event, selector, @handleActivationEvent) for event, selector of @metadata.activationEvents
handleActivationEvent: (event) =>
bubblePathEventHandlers = @disableEventHandlersOnBubblePath(event)
@activateNow()
$ ?= require('./space-pen-extensions').$
$(event.target).trigger(event)
@restoreEventHandlersOnBubblePath(bubblePathEventHandlers)
@unsubscribeFromActivationEvents()
false
unsubscribeFromActivationEvents: ->
return unless atom.workspaceView?
if _.isArray(@metadata.activationEvents)
atom.workspaceView.off(event, @handleActivationEvent) for event in @metadata.activationEvents
else if _.isString(@metadata.activationEvents)
atom.workspaceView.off(@metadata.activationEvents, @handleActivationEvent)
else
atom.workspaceView.off(event, selector, @handleActivationEvent) for event, selector of @metadata.activationEvents
disableEventHandlersOnBubblePath: (event) ->
bubblePathEventHandlers = []
disabledHandler = ->
$ ?= require('./space-pen-extensions').$
element = $(event.target)
while element.length
if eventHandlers = element.handlers()?[event.type]
for eventHandler in eventHandlers
eventHandler.disabledHandler = eventHandler.handler
eventHandler.handler = disabledHandler
bubblePathEventHandlers.push(eventHandler)
element = element.parent()
bubblePathEventHandlers
restoreEventHandlersOnBubblePath: (eventHandlers) ->
for eventHandler in eventHandlers
eventHandler.handler = eventHandler.disabledHandler
delete eventHandler.disabledHandler