diff --git a/keymaps/apple.cson b/keymaps/apple.cson
index 5087286d8..83b856c82 100644
--- a/keymaps/apple.cson
+++ b/keymaps/apple.cson
@@ -5,16 +5,24 @@
'meta-shift-down': 'core:select-to-bottom'
'.editor':
+ 'meta-left': 'editor:move-to-first-character-of-line'
'meta-right': 'editor:move-to-end-of-line'
- 'meta-left': 'editor:move-to-beginning-of-line'
- 'alt-left': 'editor:move-to-beginning-of-word'
- 'alt-right': 'editor:move-to-end-of-word'
- 'meta-shift-left': 'editor:select-to-beginning-of-line'
+ 'meta-shift-left': 'editor:select-to-first-character-of-line'
'meta-shift-right': 'editor:select-to-end-of-line'
- 'alt-shift-left': 'editor:select-to-beginning-of-word'
- 'alt-shift-right': 'editor:select-to-end-of-word'
+
+ 'home': 'editor:move-to-first-character-of-line'
+ 'end': 'editor:move-to-end-of-line'
+ 'shift-home': 'editor:select-to-first-character-of-line'
+ 'shift-end': 'editor:select-to-end-of-line'
+
+ 'alt-left': 'editor:move-to-previous-word-boundary'
+ 'alt-right': 'editor:move-to-next-word-boundary'
+ 'alt-shift-left': 'editor:select-to-previous-word-boundary'
+ 'alt-shift-right': 'editor:select-to-next-word-boundary'
+
'alt-backspace': 'editor:backspace-to-beginning-of-word'
'meta-backspace': 'editor:backspace-to-beginning-of-line'
+
'alt-delete': 'editor:delete-to-end-of-word'
'ctrl-t': 'editor:transpose'
'ctrl-A': 'editor:select-to-first-character-of-line'
diff --git a/keymaps/atom.cson b/keymaps/atom.cson
index a2448e217..2fd92b900 100644
--- a/keymaps/atom.cson
+++ b/keymaps/atom.cson
@@ -30,6 +30,7 @@
'delete': 'core:delete'
'meta-z': 'core:undo'
'meta-Z': 'core:redo'
+ 'meta-y': 'core:redo'
'meta-x': 'core:cut'
'meta-c': 'core:copy'
'meta-v': 'core:paste'
diff --git a/keymaps/editor.cson b/keymaps/editor.cson
index c0a328cb0..5e4d7cf30 100644
--- a/keymaps/editor.cson
+++ b/keymaps/editor.cson
@@ -1,5 +1,5 @@
'.editor':
- 'meta-d': 'editor:delete-line'
+ 'ctrl-K': 'editor:delete-line'
'ctrl-W': 'editor:select-word'
'meta-alt-p': 'editor:log-cursor-scope'
'meta-u': 'editor:upper-case'
@@ -38,7 +38,7 @@
'ctrl-meta-up': 'editor:move-line-up'
'ctrl-meta-down': 'editor:move-line-down'
'meta-D': 'editor:duplicate-line'
- 'ctrl-J': 'editor:join-line'
+ 'meta-j': 'editor:join-line'
'meta-<': 'editor:scroll-to-cursor'
'.editor.mini':
diff --git a/menus/base.cson b/menus/base.cson
index 64a45e615..cbe8769de 100644
--- a/menus/base.cson
+++ b/menus/base.cson
@@ -45,9 +45,96 @@
{ label: 'Paste', command: 'core:paste' }
{ label: 'Select All', command: 'core:select-all' }
{ type: 'separator' }
+ { label: 'Toggle Comments', command: 'editor:toggle-line-comments' }
+ {
+ label: 'Lines',
+ submenu: [
+ { label: 'Indent', command: 'editor:indent-selected-rows' }
+ { label: 'Outdent', command: 'editor:outdent-selected-rows' }
+ { label: 'Auto Indent', command: 'editor:auto-indent' }
+ { type: 'separator' }
+ { label: 'Move Line Up', command: 'editor:move-line-up' }
+ { label: 'Move Line Down', command: 'editor:move-line-down' }
+ { label: 'Duplicate Line', command: 'editor:duplicate-line' }
+ { label: 'Delete Line', command: 'editor:delete-line' }
+ { label: 'Join Lines', command: 'editor:join-line' }
+ ]
+ }
+ {
+ label: 'Text',
+ submenu: [
+ { label: 'Upper Case', command: 'editor:upper-case' }
+ { label: 'Lower Case', command: 'editor:lower-case' }
+ { type: 'separator' }
+ { label: 'Delete to End of Word', command: 'editor:delete-to-end-of-word' }
+ { label: 'Delete Line', command: 'editor:delete-line' }
+ { type: 'separator' }
+ { label: 'Transpose', command: 'editor:transpose' }
+ ]
+ }
+ {
+ label: 'Folding',
+ submenu: [
+ { label: 'Fold', command: 'editor:fold-current-row' }
+ { label: 'Unfold', command: 'editor:unfold-current-row' }
+ { label: 'Unfold All', command: 'editor:unfold-all' }
+ { type: 'separator' }
+ { label: 'Fold All', command: 'editor:fold-all' }
+ { label: 'Fold Level 1', command: 'editor:fold-at-indent-level-1' }
+ { label: 'Fold Level 2', command: 'editor:fold-at-indent-level-2' }
+ { label: 'Fold Level 3', command: 'editor:fold-at-indent-level-3' }
+ { label: 'Fold Level 4', command: 'editor:fold-at-indent-level-4' }
+ { label: 'Fold Level 5', command: 'editor:fold-at-indent-level-5' }
+ { label: 'Fold Level 6', command: 'editor:fold-at-indent-level-6' }
+ { label: 'Fold Level 7', command: 'editor:fold-at-indent-level-7' }
+ { label: 'Fold Level 8', command: 'editor:fold-at-indent-level-8' }
+ { label: 'Fold Level 9', command: 'editor:fold-at-indent-level-9' }
+ ]
+ }
]
}
+ {
+ label: 'Selection'
+ submenu: [
+ { label: 'Add Selection Above', command: 'editor:add-selection-above' }
+ { label: 'Add Selection Below', command: 'editor:add-selection-below' }
+ { type: 'separator' }
+ { label: 'Select to Top', command: 'core:select-to-top' }
+ { label: 'Select to Bottom', command: 'core:select-to-bottom' }
+ { type: 'separator' }
+ { label: 'Select Line', command: 'editor:select-line' }
+ { label: 'Select Word', command: 'editor:select-word' }
+ { label: 'Select to Beginning of Word', command: 'editor:select-to-beginning-of-word' }
+ { label: 'Select to Beginning of Line', command: 'editor:select-to-beginning-of-line' }
+ { label: 'Select to First Character of Line', command: 'editor:select-to-first-character-of-line' }
+ { label: 'Select to End of Word', command: 'editor:select-to-end-of-word' }
+ { label: 'Select to End of Line', command: 'editor:select-to-end-of-line' }
+ ]
+ }
+
+ {
+ label: 'Movement'
+ submenu: [
+ { label: 'Move to Top', command: 'core:move-to-top' }
+ { label: 'Move to Bottom', command: 'core:move-to-bottom' }
+ { type: 'separator' }
+ { label: 'Move to Beginning of Line', command: 'editor:move-to-beginning-of-line' }
+ { label: 'Move to First Character of Line', command: 'editor:move-to-first-character-of-line' }
+ { label: 'Move to End of Line', command: 'editor:move-to-end-of-line' }
+ { type: 'separator' }
+ { label: 'Move to Beginning of Word', command: 'editor:move-to-beginning-of-word' }
+ { label: 'Move to End of Word', command: 'editor:move-to-end-of-word' }
+ { label: 'Move to Next Word', command: 'editor:move-to-next-word-boundary' }
+ { label: 'Move to Previous Word', command: 'editor:move-to-previous-word-boundary' }
+ ]
+ }
+
+ {
+ label: 'Find'
+ submenu: []
+ }
+
{
label: 'View'
submenu: [
@@ -67,6 +154,16 @@
]
}
+ {
+ label: 'Collaboration'
+ submenu: []
+ }
+
+ {
+ label: 'Packages'
+ submenu: []
+ }
+
{
label: 'Window'
submenu: [
diff --git a/package.json b/package.json
index 33573d6f2..edafcea7d 100644
--- a/package.json
+++ b/package.json
@@ -78,9 +78,9 @@
"collaboration": "0.28.0",
"command-logger": "0.6.0",
"command-palette": "0.5.0",
- "dev-live-reload": "0.8.0",
+ "dev-live-reload": "0.11.0",
"editor-stats": "0.5.0",
- "exception-reporting": "0.4.0",
+ "exception-reporting": "0.5.0",
"find-and-replace": "0.29.0",
"fuzzy-finder": "0.15.0",
"gfm": "0.5.0",
@@ -93,14 +93,14 @@
"link": "0.7.0",
"markdown-preview": "0.11.0",
"metrics": "0.8.0",
- "package-generator": "0.13.0",
+ "package-generator": "0.14.0",
"release-notes": "0.8.0",
- "settings-view": "0.29.0",
+ "settings-view": "0.31.0",
"snippets": "0.11.0",
"spell-check": "0.8.0",
"status-bar": "0.15.0",
"styleguide": "0.9.0",
- "symbols-view": "0.12.0",
+ "symbols-view": "0.13.0",
"tabs": "0.7.0",
"terminal": "0.14.0",
"timecop": "0.7.0",
diff --git a/spec/atom-spec.coffee b/spec/atom-spec.coffee
index 65eda00c0..981036868 100644
--- a/spec/atom-spec.coffee
+++ b/spec/atom-spec.coffee
@@ -1,6 +1,7 @@
{$, $$, fs, RootView} = require 'atom'
Exec = require('child_process').exec
path = require 'path'
+ThemeManager = require '../src/theme-manager'
describe "the `atom` global", ->
beforeEach ->
@@ -20,6 +21,7 @@ describe "the `atom` global", ->
expect(pack.activateStylesheets).toHaveBeenCalled()
it "continues if the package has an invalid package.json", ->
+ spyOn(console, 'warn')
config.set("core.disabledPackages", [])
expect(-> atom.loadPackage("package-with-broken-package-json")).not.toThrow()
@@ -192,7 +194,6 @@ describe "the `atom` global", ->
expect(atom.contextMenu.definitionsForElement(element)[1].label).toBe "Menu item 1"
expect(atom.contextMenu.definitionsForElement(element)[2]).toBeUndefined()
-
describe "stylesheet loading", ->
describe "when the metadata contains a 'stylesheets' manifest", ->
it "loads stylesheets from the stylesheets directory as specified by the manifest", ->
@@ -336,3 +337,92 @@ describe "the `atom` global", ->
atom.activatePackage('language-ruby', sync: true)
atom.deactivatePackage('language-ruby')
expect(syntax.getProperty(['.source.ruby'], 'editor.commentStart')).toBeUndefined()
+
+ describe ".activate()", ->
+ packageActivator = null
+ themeActivator = null
+
+ beforeEach ->
+ spyOn(console, 'warn')
+ atom.packages.loadPackages()
+
+ loadedPackages = atom.packages.getLoadedPackages()
+ expect(loadedPackages.length).toBeGreaterThan 0
+
+ packageActivator = spyOn(atom.packages, 'activatePackages')
+ themeActivator = spyOn(atom.themes, 'activatePackages')
+
+ afterEach ->
+ atom.packages.unloadPackages()
+
+ Syntax = require '../src/syntax'
+ atom.syntax = window.syntax = new Syntax()
+
+ it "activates all the packages, and none of the themes", ->
+ atom.packages.activate()
+
+ expect(packageActivator).toHaveBeenCalled()
+ expect(themeActivator).toHaveBeenCalled()
+
+ packages = packageActivator.mostRecentCall.args[0]
+ expect(['atom', 'textmate']).toContain(pack.getType()) for pack in packages
+
+ themes = themeActivator.mostRecentCall.args[0]
+ expect(['theme']).toContain(theme.getType()) for theme in themes
+
+ describe ".en/disablePackage()", ->
+ describe "with packages", ->
+ it ".enablePackage() enables a disabled package", ->
+ packageName = 'package-with-main'
+ atom.config.pushAtKeyPath('core.disabledPackages', packageName)
+ atom.packages.observeDisabledPackages()
+ expect(config.get('core.disabledPackages')).toContain packageName
+
+ pack = atom.packages.enablePackage(packageName)
+
+ loadedPackages = atom.packages.getLoadedPackages()
+ activatedPackages = atom.packages.getActivePackages()
+ expect(loadedPackages).toContain(pack)
+ expect(activatedPackages).toContain(pack)
+ expect(config.get('core.disabledPackages')).not.toContain packageName
+
+ it ".disablePackage() disables an enabled package", ->
+ packageName = 'package-with-main'
+ atom.packages.activatePackage(packageName)
+ atom.packages.observeDisabledPackages()
+ expect(config.get('core.disabledPackages')).not.toContain packageName
+
+ pack = atom.packages.disablePackage(packageName)
+
+ activatedPackages = atom.packages.getActivePackages()
+ expect(activatedPackages).not.toContain(pack)
+ expect(config.get('core.disabledPackages')).toContain packageName
+
+ describe "with themes", ->
+ beforeEach ->
+ atom.themes.activateThemes()
+
+ afterEach ->
+ atom.themes.deactivateThemes()
+ atom.config.unobserve('core.themes')
+
+ it ".enablePackage() and .disablePackage() enables and disables a theme", ->
+ packageName = 'theme-with-package-file'
+
+ expect(config.get('core.themes')).not.toContain packageName
+ expect(config.get('core.disabledPackages')).not.toContain packageName
+
+ # enabling of theme
+ pack = atom.packages.enablePackage(packageName)
+ activatedPackages = atom.packages.getActivePackages()
+ expect(activatedPackages).toContain(pack)
+ expect(config.get('core.themes')).toContain packageName
+ expect(config.get('core.disabledPackages')).not.toContain packageName
+
+ # disabling of theme
+ pack = atom.packages.disablePackage(packageName)
+ activatedPackages = atom.packages.getActivePackages()
+ expect(activatedPackages).not.toContain(pack)
+ expect(config.get('core.themes')).not.toContain packageName
+ expect(config.get('core.themes')).not.toContain packageName
+ expect(config.get('core.disabledPackages')).not.toContain packageName
diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee
index 277a8a2d3..f3a3d6e79 100644
--- a/spec/config-spec.coffee
+++ b/spec/config-spec.coffee
@@ -32,7 +32,7 @@ describe "Config", ->
config.set("foo.bar.baz", 42)
expect(config.save).toHaveBeenCalled()
- expect(observeHandler).toHaveBeenCalledWith 42
+ expect(observeHandler).toHaveBeenCalledWith 42, {previous: undefined}
describe "when the value equals the default value", ->
it "does not store the value", ->
@@ -54,7 +54,7 @@ describe "Config", ->
expect(config.pushAtKeyPath("foo.bar.baz", "b")).toBe 2
expect(config.get("foo.bar.baz")).toEqual ["a", "b"]
- expect(observeHandler).toHaveBeenCalledWith config.get("foo.bar.baz")
+ expect(observeHandler).toHaveBeenCalledWith config.get("foo.bar.baz"), {previous: ['a']}
describe ".removeAtKeyPath(keyPath, value)", ->
it "removes the given value from the array at the key path and updates observers", ->
@@ -65,7 +65,7 @@ describe "Config", ->
expect(config.removeAtKeyPath("foo.bar.baz", "b")).toEqual ["a", "c"]
expect(config.get("foo.bar.baz")).toEqual ["a", "c"]
- expect(observeHandler).toHaveBeenCalledWith config.get("foo.bar.baz")
+ expect(observeHandler).toHaveBeenCalledWith config.get("foo.bar.baz"), {previous: ['a', 'b', 'c']}
describe ".getPositiveInt(keyPath, defaultValue)", ->
it "returns the proper current or default value", ->
@@ -142,26 +142,26 @@ describe "Config", ->
it "fires the callback every time the observed value changes", ->
observeHandler.reset() # clear the initial call
config.set('foo.bar.baz', "value 2")
- expect(observeHandler).toHaveBeenCalledWith("value 2")
+ expect(observeHandler).toHaveBeenCalledWith("value 2", {previous: 'value 1'})
observeHandler.reset()
config.set('foo.bar.baz', "value 1")
- expect(observeHandler).toHaveBeenCalledWith("value 1")
+ expect(observeHandler).toHaveBeenCalledWith("value 1", {previous: 'value 2'})
it "fires the callback when the observed value is deleted", ->
observeHandler.reset() # clear the initial call
config.set('foo.bar.baz', undefined)
- expect(observeHandler).toHaveBeenCalledWith(undefined)
+ expect(observeHandler).toHaveBeenCalledWith(undefined, {previous: 'value 1'})
it "fires the callback when the full key path goes into and out of existence", ->
observeHandler.reset() # clear the initial call
config.set("foo.bar", undefined)
- expect(observeHandler).toHaveBeenCalledWith(undefined)
+ expect(observeHandler).toHaveBeenCalledWith(undefined, {previous: 'value 1'})
observeHandler.reset()
config.set("foo.bar.baz", "i'm back")
- expect(observeHandler).toHaveBeenCalledWith("i'm back")
+ expect(observeHandler).toHaveBeenCalledWith("i'm back", {previous: undefined})
describe ".initializeConfigDirectory()", ->
beforeEach ->
diff --git a/spec/space-pen-extensions-spec.coffee b/spec/space-pen-extensions-spec.coffee
index 8ae85f814..f121a683f 100644
--- a/spec/space-pen-extensions-spec.coffee
+++ b/spec/space-pen-extensions-spec.coffee
@@ -25,7 +25,7 @@ describe "SpacePen extensions", ->
config.set("foo.bar", "hello")
- expect(observeHandler).toHaveBeenCalledWith("hello")
+ expect(observeHandler).toHaveBeenCalledWith("hello", previous: undefined)
observeHandler.reset()
view.unobserveConfig()
diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee
index d5d5cc1b3..250dc2593 100644
--- a/spec/theme-manager-spec.coffee
+++ b/spec/theme-manager-spec.coffee
@@ -8,10 +8,25 @@ describe "ThemeManager", ->
themeManager = null
beforeEach ->
- themeManager = new ThemeManager()
+ themeManager = new ThemeManager(atom.packages)
afterEach ->
- themeManager.unload()
+ themeManager.deactivateThemes()
+
+ describe "theme getters and setters", ->
+ beforeEach ->
+ atom.packages.loadPackages()
+
+ it 'getLoadedThemes get all the loaded themes', ->
+ themes = themeManager.getLoadedThemes()
+ expect(themes.length).toBeGreaterThan(2)
+
+ it 'getActiveThemes get all the active themes', ->
+ themeManager.activateThemes()
+ names = atom.config.get('core.themes')
+ expect(names.length).toBeGreaterThan(0)
+ themes = themeManager.getActiveThemes()
+ expect(themes).toHaveLength(names.length)
describe "getImportPaths()", ->
it "returns the theme directories before the themes are loaded", ->
@@ -32,7 +47,7 @@ describe "ThemeManager", ->
it "add/removes stylesheets to reflect the new config value", ->
themeManager.on 'reloaded', reloadHandler = jasmine.createSpy()
spyOn(themeManager, 'getUserStylesheetPath').andCallFake -> null
- themeManager.load()
+ themeManager.activateThemes()
config.set('core.themes', [])
expect($('style.theme').length).toBe 0
@@ -59,21 +74,7 @@ describe "ThemeManager", ->
describe "when a theme fails to load", ->
it "logs a warning", ->
spyOn(console, 'warn')
- themeManager.activateTheme('a-theme-that-will-not-be-found')
- expect(console.warn).toHaveBeenCalled()
-
- describe "theme-loaded event", ->
- beforeEach ->
- spyOn(themeManager, 'getUserStylesheetPath').andCallFake -> null
- themeManager.load()
-
- it "fires when a new theme has been added", ->
- themeManager.on 'theme-activated', loadHandler = jasmine.createSpy()
-
- config.set('core.themes', ['atom-dark-syntax'])
-
- expect(loadHandler).toHaveBeenCalled()
- expect(loadHandler.mostRecentCall.args[0]).toBeInstanceOf AtomPackage
+ expect(-> atom.packages.activatePackage('a-theme-that-will-not-be-found')).toThrow()
describe "requireStylesheet(path)", ->
it "synchronously loads css at the given path and installs a style tag for it in the head", ->
@@ -140,7 +141,7 @@ describe "ThemeManager", ->
window.rootView = new RootView
rootView.append $$ -> @div class: 'editor'
rootView.attachToDom()
- themeManager.load()
+ themeManager.activateThemes()
it "loads the correct values from the theme's ui-variables file", ->
config.set('core.themes', ['theme-with-ui-variables'])
diff --git a/src/atom-package.coffee b/src/atom-package.coffee
index 62f7cdc70..579f17085 100644
--- a/src/atom-package.coffee
+++ b/src/atom-package.coffee
@@ -25,20 +25,18 @@ class AtomPackage extends Package
resolvedMainModulePath: false
mainModule: null
+ constructor: (path, {@metadata}) ->
+ super(path)
+ @reset()
+
getType: -> 'atom'
- load: ->
- @metadata = {}
- @stylesheets = []
- @keymaps = []
- @menus = []
- @grammars = []
- @scopedProperties = []
+ getStylesheetType: -> 'bundled'
+ load: ->
@measure 'loadTime', =>
try
- @metadata = Package.loadMetadata(@path)
- return if @isTheme()
+ @metadata ?= Package.loadMetadata(@path)
@loadKeymaps()
@loadMenus()
@@ -55,9 +53,21 @@ class AtomPackage extends Package
console.warn "Failed to load package named '#{@name}'", e.stack ? e
this
+ enable: ->
+ atom.config.removeAtKeyPath('core.disabledPackages', @metadata.name)
+
+ disable: ->
+ atom.config.pushAtKeyPath('core.disabledPackages', @metadata.name)
+
+ reset: ->
+ @stylesheets = []
+ @keymaps = []
+ @menus = []
+ @grammars = []
+ @scopedProperties = []
+
activate: ({immediate}={}) ->
@measure 'activateTime', =>
- @loadStylesheets() if @isTheme()
@activateResources()
if @metadata.activationEvents? and not immediate
@subscribeToActivationEvents()
@@ -69,7 +79,7 @@ class AtomPackage extends Package
@activateConfig()
@activateStylesheets()
if @requireMainModule()
- @mainModule.activate(atom.getPackageState(@name) ? {})
+ @mainModule.activate(atom.packages.getPackageState(@name) ? {})
@mainActivated = true
catch e
console.warn "Failed to activate package named '#{@name}'", e.stack
@@ -86,7 +96,7 @@ class AtomPackage extends Package
activateStylesheets: ->
return if @stylesheetsActivated
- type = if @metadata.theme then 'theme' else 'bundled'
+ type = @getStylesheetType()
for [stylesheetPath, content] in @stylesheets
atom.themes.applyStylesheet(stylesheetPath, content, type)
@stylesheetsActivated = true
@@ -183,8 +193,7 @@ class AtomPackage extends Package
@reloadStylesheet(stylesheetPath, content) for [stylesheetPath, content] in @stylesheets
reloadStylesheet: (stylesheetPath, content) ->
- type = if @metadata.theme then 'theme' else 'bundled'
- atom.themes.applyStylesheet(stylesheetPath, content, type)
+ atom.themes.applyStylesheet(stylesheetPath, content, @getStylesheetType())
requireMainModule: ->
return @mainModule if @mainModule?
diff --git a/src/atom.coffee b/src/atom.coffee
index 8aadbbe5b..ffe3ba324 100644
--- a/src/atom.coffee
+++ b/src/atom.coffee
@@ -55,7 +55,7 @@ class Atom
@__defineSetter__ 'packageStates', (packageStates) => @packages.packageStates = packageStates
@subscribe @packages, 'loaded', => @watchThemes()
- @themes = new ThemeManager()
+ @themes = new ThemeManager(@packages)
@contextMenu = new ContextMenuManager(devMode)
@menu = new MenuManager()
@pasteboard = new Pasteboard()
@@ -220,6 +220,9 @@ class Atom
inDevMode: ->
@getLoadSettings().devMode
+ inSpecMode: ->
+ @getLoadSettings().isSpec
+
toggleFullScreen: ->
@setFullScreen(!@isFullScreen())
diff --git a/src/config.coffee b/src/config.coffee
index 1f38c30c8..18cbf2692 100644
--- a/src/config.coffee
+++ b/src/config.coffee
@@ -193,7 +193,7 @@ class Config
# `callback` is fired whenever the value of the key is changed and will
# be fired immediately unless the `callNow` option is `false`.
#
- # keyPath - The {String} name of the key to watch
+ # keyPath - The {String} name of the key to observe
# options - An optional {Object} containing the `callNow` key.
# callback - The {Function} that fires when the. It is given a single argument, `value`,
# which is the new value of `keyPath`.
@@ -207,14 +207,22 @@ class Config
updateCallback = =>
value = @get(keyPath)
unless _.isEqual(value, previousValue)
+ previous = previousValue
previousValue = _.clone(value)
- callback(value)
+ callback(value, {previous})
subscription = { cancel: => @off 'updated', updateCallback }
- @on 'updated', updateCallback
+ @on "updated.#{keyPath.replace(/\./, '-')}", updateCallback
callback(value) if options.callNow ? true
subscription
+
+ # Public: Unobserve all callbacks on a given key
+ #
+ # keyPath - The {String} name of the key to unobserve
+ unobserve: (keyPath) ->
+ @off("updated.#{keyPath.replace(/\./, '-')}")
+
# Private:
update: ->
return if @configFileHasErrors
diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee
index 7fa1f242a..76d171c60 100644
--- a/src/menu-manager.coffee
+++ b/src/menu-manager.coffee
@@ -30,7 +30,10 @@ class MenuManager
# Public: Refreshes the currently visible menu.
update: ->
- @sendToBrowserProcess(@template, atom.keymap.keystrokesByCommandForSelector('body'))
+ keystrokesByCommand = atom.keymap.keystrokesByCommandForSelector('body')
+ _.extend(keystrokesByCommand, atom.keymap.keystrokesByCommandForSelector('.editor'))
+ _.extend(keystrokesByCommand, atom.keymap.keystrokesByCommandForSelector('.editor:not(.mini)'))
+ @sendToBrowserProcess(@template, keystrokesByCommand)
# Private
loadCoreItems: ->
@@ -49,6 +52,20 @@ class MenuManager
else
menu.push(item) unless _.find(menu, (i) -> i.label == item.label)
+ # Private: OSX can't handle displaying accelerators for multiple keystrokes.
+ # If they are sent across, it will stop processing accelerators for the rest
+ # of the menu items.
+ filterMultipleKeystrokes: (keystrokesByCommand) ->
+ filtered = {}
+ for key, bindings of keystrokesByCommand
+ for binding in bindings
+ continue if binding.indexOf(' ') != -1
+
+ filtered[key] ?= []
+ filtered[key].push(binding)
+ filtered
+
# Private
sendToBrowserProcess: (template, keystrokesByCommand) ->
+ keystrokesByCommand = @filterMultipleKeystrokes(keystrokesByCommand)
ipc.sendChannel 'update-application-menu', template, keystrokesByCommand
diff --git a/src/package-manager.coffee b/src/package-manager.coffee
index 66f06d2ef..65987ab80 100644
--- a/src/package-manager.coffee
+++ b/src/package-manager.coffee
@@ -4,6 +4,21 @@ _ = require 'underscore-plus'
Package = require './package'
path = require 'path'
+###
+Packages have a lifecycle
+
+* The paths to all non-disabled packages and themes are found on disk (these are available packages)
+* Every package (except those in core.disabledPackages) is 'loaded', meaning
+ `Package` objects are created, and their metadata loaded. This includes themes,
+ as themes are packages
+* The ThemeManager.activateThemes() is called 'activating' all the themes, meaning
+ their stylesheets are loaded into the window.
+* The PackageManager.activatePackages() function is called 'activating' non-theme
+ package, meaning its resources -- keymaps, classes, etc. -- are loaded, and
+ the package's activate() method is called.
+* Packages and themes can then be enabled and disabled via the public
+ .enablePackage(name) and .disablePackage(name) functions.
+###
module.exports =
class PackageManager
Emitter.includeInto(this)
@@ -16,6 +31,14 @@ class PackageManager
@loadedPackages = {}
@activePackages = {}
@packageStates = {}
+ @observingDisabledPackages = false
+
+ @packageActivators = []
+ @registerPackageActivator(this, ['atom', 'textmate'])
+
+ # Public: Get the path to the apm command
+ getApmPath: ->
+ @apmPath ?= require.resolve('atom-package-manager/bin/apm')
getPackageState: (name) ->
@packageStates[name]
@@ -23,10 +46,37 @@ class PackageManager
setPackageState: (name, state) ->
@packageStates[name] = state
- activatePackages: ->
- @activatePackage(pack.name) for pack in @getLoadedPackages()
+ # Public:
+ enablePackage: (name) ->
+ pack = @loadPackage(name)
+ pack?.enable()
+ pack
+ # Public:
+ disablePackage: (name) ->
+ pack = @loadPackage(name)
+ pack?.disable()
+ pack
+
+ # Internal-only: Activate all the packages that should be activated.
+ activate: ->
+ for [activator, types] in @packageActivators
+ packages = @getLoadedPackagesForTypes(types)
+ activator.activatePackages(packages)
+
+ # Public: another type of package manager can handle other package types.
+ # See ThemeManager
+ registerPackageActivator: (activator, types) ->
+ @packageActivators.push([activator, types])
+
+ # Internal-only:
+ activatePackages: (packages) ->
+ @activatePackage(pack.name) for pack in packages
+ @observeDisabledPackages()
+
+ # Internal-only: Activate a single package by name
activatePackage: (name, options) ->
+ return pack if pack = @getActivePackage(name)
if pack = @loadPackage(name, options)
@activePackages[pack.name] = pack
pack.activate(options)
@@ -34,6 +84,7 @@ class PackageManager
deactivatePackages: ->
@deactivatePackage(pack.name) for pack in @getActivePackages()
+ @unobserveDisabledPackages()
deactivatePackage: (name) ->
if pack = @getActivePackage(name)
@@ -43,14 +94,32 @@ class PackageManager
else
throw new Error("No active package for name '#{name}'")
+ getActivePackages: ->
+ _.values(@activePackages)
+
getActivePackage: (name) ->
@activePackages[name]
isPackageActive: (name) ->
@getActivePackage(name)?
- getActivePackages: ->
- _.values(@activePackages)
+ unobserveDisabledPackages: ->
+ return unless @observingDisabledPackages
+ config.unobserve('core.disabledPackages')
+ @observingDisabledPackages = false
+
+ observeDisabledPackages: ->
+ return if @observingDisabledPackages
+
+ config.observe 'core.disabledPackages', callNow: false, (disabledPackages, {previous}) =>
+ packagesToEnable = _.difference(previous, disabledPackages)
+ packagesToDisable = _.difference(disabledPackages, previous)
+
+ @deactivatePackage(packageName) for packageName in packagesToDisable when @getActivePackage(packageName)
+ @activatePackage(packageName) for packageName in packagesToEnable
+ null
+
+ @observingDisabledPackages = true
loadPackages: ->
# Ensure atom exports is already in the require cache so the load time
@@ -61,21 +130,19 @@ class PackageManager
@emit 'loaded'
loadPackage: (name, options) ->
- if @isPackageDisabled(name)
- return console.warn("Tried to load disabled package '#{name}'")
-
if packagePath = @resolvePackagePath(name)
return pack if pack = @getLoadedPackage(name)
pack = Package.load(packagePath, options)
- if pack.metadata?.theme
- atom.themes.register(pack)
- else
- @loadedPackages[pack.name] = pack
+ @loadedPackages[pack.name] = pack if pack?
pack
else
throw new Error("Could not resolve '#{name}' to a package path")
+ unloadPackages: ->
+ @unloadPackage(name) for name in _.keys(@loadedPackages)
+ null
+
unloadPackage: (name) ->
if @isPackageActive(name)
throw new Error("Tried to unload active package '#{name}'")
@@ -85,19 +152,6 @@ class PackageManager
else
throw new Error("No loaded package for name '#{name}'")
- resolvePackagePath: (name) ->
- return name if fsUtils.isDirectorySync(name)
-
- packagePath = fsUtils.resolve(@packageDirPaths..., name)
- return packagePath if fsUtils.isDirectorySync(packagePath)
-
- packagePath = path.join(@resourcePath, 'node_modules', name)
- return packagePath if @isInternalPackage(packagePath)
-
- isInternalPackage: (packagePath) ->
- {engines} = Package.loadMetadata(packagePath, true)
- engines?.atom?
-
getLoadedPackage: (name) ->
@loadedPackages[name]
@@ -107,9 +161,28 @@ class PackageManager
getLoadedPackages: ->
_.values(@loadedPackages)
+ # Private: Get packages for a certain package type
+ #
+ # * types: an {Array} of {String}s like ['atom', 'textmate']
+ getLoadedPackagesForTypes: (types) ->
+ pack for pack in @getLoadedPackages() when pack.getType() in types
+
+ resolvePackagePath: (name) ->
+ return name if fsUtils.isDirectorySync(name)
+
+ packagePath = fsUtils.resolve(@packageDirPaths..., name)
+ return packagePath if fsUtils.isDirectorySync(packagePath)
+
+ packagePath = path.join(@resourcePath, 'node_modules', name)
+ return packagePath if @isInternalPackage(packagePath)
+
isPackageDisabled: (name) ->
_.include(config.get('core.disabledPackages') ? [], name)
+ isInternalPackage: (packagePath) ->
+ {engines} = Package.loadMetadata(packagePath, true)
+ engines?.atom?
+
getAvailablePackagePaths: ->
packagePaths = []
diff --git a/src/package.coffee b/src/package.coffee
index 4a1a2571f..4051ff504 100644
--- a/src/package.coffee
+++ b/src/package.coffee
@@ -7,15 +7,25 @@ class Package
@build: (path) ->
TextMatePackage = require './text-mate-package'
AtomPackage = require './atom-package'
+ ThemePackage = require './theme-package'
if TextMatePackage.testName(path)
- new TextMatePackage(path)
+ pack = new TextMatePackage(path)
else
- new AtomPackage(path)
+ try
+ metadata = @loadMetadata(path)
+ if metadata.theme
+ pack = new ThemePackage(path, {metadata})
+ else
+ pack = new AtomPackage(path, {metadata})
+ catch e
+ console.warn "Failed to load package.json '#{basename(path)}'", e.stack ? e
+
+ pack
@load: (path, options) ->
pack = @build(path)
- pack.load(options)
+ pack?.load(options)
pack
@loadMetadata: (path, ignoreErrors=false) ->
diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee
index c3e097581..f40421683 100644
--- a/src/theme-manager.coffee
+++ b/src/theme-manager.coffee
@@ -8,31 +8,89 @@ _ = require 'underscore-plus'
fsUtils = require './fs-utils'
# Private: Handles discovering and loading available themes.
+###
+Themes are a subset of packages
+###
module.exports =
class ThemeManager
Emitter.includeInto(this)
- constructor: ->
- @loadedThemes = []
- @activeThemes = []
+ constructor: (@packageManager) ->
@lessCache = null
-
- # Internal-only:
- register: (theme) ->
- @loadedThemes.push(theme)
- theme
+ @packageManager.registerPackageActivator(this, ['theme'])
# Internal-only:
getAvailableNames: ->
- _.map @loadedThemes, (theme) -> theme.metadata.name
+ # TODO: Maybe should change to list all the available themes out there?
+ @getLoadedNames()
+
+ getLoadedNames: ->
+ theme.name for theme in @getLoadedThemes()
+
+ # Internal-only:
+ getActiveNames: ->
+ theme.name for theme in @getActiveThemes()
# Internal-only:
getActiveThemes: ->
- _.clone(@activeThemes)
+ pack for pack in @packageManager.getActivePackages() when pack.isTheme()
# Internal-only:
getLoadedThemes: ->
- _.clone(@loadedThemes)
+ pack for pack in @packageManager.getLoadedPackages() when pack.isTheme()
+
+ # Internal-only: adhere to the PackageActivator interface
+ activatePackages: (themePackages) -> @activateThemes()
+
+ # Internal-only:
+ activateThemes: ->
+ # atom.config.observe runs the callback once, then on subsequent changes.
+ atom.config.observe 'core.themes', (themeNames) =>
+ @deactivateThemes()
+ themeNames = [themeNames] unless _.isArray(themeNames)
+
+ # Reverse so the first (top) theme is loaded after the others. We want
+ # the first/top theme to override later themes in the stack.
+ themeNames = _.clone(themeNames).reverse()
+
+ @packageManager.activatePackage(themeName) for themeName in themeNames
+ @loadUserStylesheet()
+ @reloadBaseStylesheets()
+ @emit('reloaded')
+
+ # Internal-only:
+ deactivateThemes: ->
+ @removeStylesheet(@userStylesheetPath) if @userStylesheetPath?
+ @packageManager.deactivatePackage(pack.name) for pack in @getActiveThemes()
+ null
+
+ # Public:
+ getImportPaths: ->
+ activeThemes = @getActiveThemes()
+ if activeThemes.length > 0
+ themePaths = (theme.getStylesheetsPath() for theme in activeThemes when theme)
+ else
+ themePaths = []
+ for themeName in atom.config.get('core.themes') ? []
+ if themePath = @packageManager.resolvePackagePath(themeName)
+ themePaths.push(path.join(themePath, AtomPackage.stylesheetsDir))
+
+ themePath for themePath in themePaths when fsUtils.isDirectorySync(themePath)
+
+ # Public:
+ getUserStylesheetPath: ->
+ stylesheetPath = fsUtils.resolve(path.join(atom.config.configDirPath, 'user'), ['css', 'less'])
+ if fsUtils.isFileSync(stylesheetPath)
+ stylesheetPath
+ else
+ null
+
+ # Private:
+ loadUserStylesheet: ->
+ if userStylesheetPath = @getUserStylesheetPath()
+ @userStylesheetPath = userStylesheetPath
+ userStylesheetContents = @loadStylesheet(userStylesheetPath)
+ @applyStylesheet(userStylesheetPath, userStylesheetContents, 'userTheme')
# Internal-only:
loadBaseStylesheets: ->
@@ -109,91 +167,3 @@ class ThemeManager
$("head style.#{ttype}:last").after ""
else
$("head").append ""
-
- # Internal-only:
- unload: ->
- @removeStylesheet(@userStylesheetPath) if @userStylesheetPath?
- theme.deactivate() while theme = @activeThemes.pop()
-
- # Internal-only:
- load: ->
- config.observe 'core.themes', (themeNames) =>
- @unload()
- themeNames = [themeNames] unless _.isArray(themeNames)
-
- # Reverse so the first (top) theme is loaded after the others. We want
- # the first/top theme to override later themes in the stack.
- themeNames = _.clone(themeNames).reverse()
-
- @activateTheme(themeName) for themeName in themeNames
- @loadUserStylesheet()
- @reloadBaseStylesheets()
- @emit('reloaded')
-
- # Private:
- loadTheme: (name, options) ->
- if themePath = @resolveThemePath(name)
- return theme if theme = @getLoadedTheme(name)
- pack = Package.load(themePath, options)
- if pack.isTheme()
- @register(pack)
- else
- throw new Error("Attempted to load a non-theme package '#{name}' as a theme")
- else
- throw new Error("Could not resolve '#{name}' to a theme path")
-
- # Private:
- getLoadedTheme: (name) ->
- _.find @loadedThemes, (theme) -> theme.metadata.name is name
-
- # Private:
- resolveThemePath: (name) ->
- return name if fsUtils.isDirectorySync(name)
-
- packagePath = fsUtils.resolve(config.packageDirPaths..., name)
- return packagePath if fsUtils.isDirectorySync(packagePath)
-
- packagePath = path.join(window.resourcePath, 'node_modules', name)
- return packagePath if @isThemePath(packagePath)
-
- # Private:
- isThemePath: (packagePath) ->
- {engines, theme} = Package.loadMetadata(packagePath, true)
- engines?.atom? and theme
-
- # Private:
- activateTheme: (name) ->
- try
- theme = @loadTheme(name)
- theme.activate()
- @activeThemes.push(theme)
- @emit('theme-activated', theme)
- catch error
- console.warn("Failed to load theme #{name}", error.stack ? error)
-
- # Public:
- getUserStylesheetPath: ->
- stylesheetPath = fsUtils.resolve(path.join(config.configDirPath, 'user'), ['css', 'less'])
- if fsUtils.isFileSync(stylesheetPath)
- stylesheetPath
- else
- null
-
- # Public:
- getImportPaths: ->
- if @activeThemes.length > 0
- themePaths = (theme.getStylesheetsPath() for theme in @activeThemes when theme)
- else
- themePaths = []
- for themeName in config.get('core.themes') ? []
- if themePath = @resolveThemePath(themeName)
- themePaths.push(path.join(themePath, AtomPackage.stylesheetsDir))
-
- themePath for themePath in themePaths when fsUtils.isDirectorySync(themePath)
-
- # Private:
- loadUserStylesheet: ->
- if userStylesheetPath = @getUserStylesheetPath()
- @userStylesheetPath = userStylesheetPath
- userStylesheetContents = @loadStylesheet(userStylesheetPath)
- @applyStylesheet(userStylesheetPath, userStylesheetContents, 'userTheme')
diff --git a/src/theme-package.coffee b/src/theme-package.coffee
new file mode 100644
index 000000000..73a89e5cf
--- /dev/null
+++ b/src/theme-package.coffee
@@ -0,0 +1,32 @@
+AtomPackage = require './atom-package'
+Package = require './package'
+
+### Internal: Loads and resolves packages. ###
+
+module.exports =
+class ThemePackage extends AtomPackage
+
+ getType: -> 'theme'
+
+ getStylesheetType: -> 'theme'
+
+ enable: ->
+ themes = atom.config.get('core.themes')
+ themes = [@metadata.name].concat(themes)
+ atom.config.set('core.themes', themes)
+
+ disable: ->
+ atom.config.removeAtKeyPath('core.themes', @metadata.name)
+
+ load: ->
+ @measure 'loadTime', =>
+ try
+ @metadata ?= Package.loadMetadata(@path)
+ catch e
+ console.warn "Failed to load theme named '#{@name}'", e.stack ? e
+ this
+
+ activate: ->
+ @measure 'activateTime', =>
+ @loadStylesheets()
+ @activateNow()
diff --git a/src/window.coffee b/src/window.coffee
index 9678ca715..4282d2a0b 100644
--- a/src/window.coffee
+++ b/src/window.coffee
@@ -51,9 +51,8 @@ window.startEditorWindow = ->
atom.keymap.loadBundledKeymaps()
atom.themes.loadBaseStylesheets()
atom.packages.loadPackages()
- atom.themes.load()
deserializeEditorWindow()
- atom.packages.activatePackages()
+ atom.packages.activate()
atom.keymap.loadUserKeymaps()
atom.requireUserInitScript()
atom.menu.update()
diff --git a/tasks/publish-packages.coffee b/tasks/publish-packages.coffee
index 3f274919c..765a927f1 100644
--- a/tasks/publish-packages.coffee
+++ b/tasks/publish-packages.coffee
@@ -6,6 +6,8 @@ request = require 'request'
# This task should be run whenever you want to be sure that atom.io contains
# all the packages and versions referenced in Atom's package.json file.
module.exports = (grunt) ->
+ {spawn} = require('./task-helpers')(grunt)
+
baseUrl = "https://www.atom.io/api/packages"
packageExists = (packageName, token, callback) ->
@@ -61,38 +63,46 @@ module.exports = (grunt) ->
else
callback()
- grunt.registerTask 'publish-packages', 'Publish all bundled packages', ->
- token = process.env.ATOM_ACCESS_TOKEN
- unless token
- grunt.log.error('Must set ATOM_ACCESS_TOKEN environment variable')
- return false
+ getToken = (callback) ->
+ if token = process.env.ATOM_ACCESS_TOKEN
+ callback(null, token)
+ else
+ spawn {cmd: 'security', args: ['-q', 'find-generic-password', '-ws', 'GitHub API Token']}, (error, result, code) ->
+ token = result.toString() unless error?
+ callback(error, token)
- {packageDependencies} = grunt.file.readJSON('package.json') ? {}
+ grunt.registerTask 'publish-packages', 'Publish all bundled packages', ->
done = @async()
- tasks = []
- for name, version of packageDependencies
- do (name, version) ->
- tasks.push (callback) ->
- grunt.log.writeln("Publishing #{name}@#{version}")
- tag = "v#{version}"
- packageExists name, token, (error, exists) ->
- if error?
- callback(error)
- return
-
- if exists
- createPackage name, token, (error) ->
- if error?
- callback(error)
- else
- createPackageVersion(name, tag, token, callback)
- else
- createPackageVersion(name, tag, token, callback)
-
- async.waterfall tasks, (error) ->
- if error?
- grunt.log.error(error.message)
+ getToken (error, token) ->
+ unless token
+ grunt.log.error('Token not found in keychain or ATOM_ACCESS_TOKEN environment variable')
done(false)
- else
- done()
+
+ {packageDependencies} = grunt.file.readJSON('package.json') ? {}
+ tasks = []
+ for name, version of packageDependencies
+ do (name, version) ->
+ tasks.push (callback) ->
+ grunt.log.writeln("Publishing #{name}@#{version}")
+ tag = "v#{version}"
+ packageExists name, token, (error, exists) ->
+ if error?
+ callback(error)
+ return
+
+ if exists
+ createPackage name, token, (error) ->
+ if error?
+ callback(error)
+ else
+ createPackageVersion(name, tag, token, callback)
+ else
+ createPackageVersion(name, tag, token, callback)
+
+ async.waterfall tasks, (error) ->
+ if error?
+ grunt.log.error(error.message)
+ done(false)
+ else
+ done()
diff --git a/vendor/apm b/vendor/apm
index 08f26c2ed..0bd38255e 160000
--- a/vendor/apm
+++ b/vendor/apm
@@ -1 +1 @@
-Subproject commit 08f26c2edb8d041b9cd8374c6e901fb9c026e6ad
+Subproject commit 0bd38255ea0aa0fec2a932177b30a388c2d16ded