mirror of
https://github.com/atom/atom.git
synced 2026-01-23 22:08:08 -05:00
343 lines
11 KiB
CoffeeScript
343 lines
11 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()
|
|
@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: ->
|
|
@grammarsPromise ?= @loadGrammars()
|
|
|
|
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
|
|
|
|
unless @grammarsActivated
|
|
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'])
|
|
|
|
loadGrammarsSync: ->
|
|
return if @grammarsLoaded
|
|
|
|
grammarsDirPath = path.join(@path, 'grammars')
|
|
grammarPaths = fs.listSync(grammarsDirPath, ['json', 'cson'])
|
|
for grammarPath in grammarPaths
|
|
try
|
|
grammar = atom.syntax.readGrammarSync(grammarPath)
|
|
grammar.packageName = @name
|
|
@grammars.push(grammar)
|
|
grammar.activate()
|
|
catch error
|
|
console.warn("Failed to load grammar: #{grammarPath}", error.stack ? error)
|
|
|
|
@grammarsLoaded = true
|
|
@grammarsActivated = true
|
|
|
|
loadGrammars: ->
|
|
return Q() if @grammarsLoaded
|
|
|
|
loadGrammar = (grammarPath, callback) =>
|
|
atom.syntax.readGrammar grammarPath, (error, grammar) =>
|
|
if error?
|
|
console.warn("Failed to load grammar: #{grammarPath}", error.stack ? error)
|
|
else
|
|
grammar.packageName = @name
|
|
@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
|