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