diff --git a/package.json b/package.json index 33573d6f2..a43af2dc0 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "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", "find-and-replace": "0.29.0", @@ -95,7 +95,7 @@ "metrics": "0.8.0", "package-generator": "0.13.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", 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/package-manager.coffee b/src/package-manager.coffee index 66f06d2ef..a94153aa9 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,10 @@ class PackageManager @loadedPackages = {} @activePackages = {} @packageStates = {} + @observingDisabledPackages = false + + @packageActivators = [] + @registerPackageActivator(this, ['atom', 'textmate']) getPackageState: (name) -> @packageStates[name] @@ -23,10 +42,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 +80,7 @@ class PackageManager deactivatePackages: -> @deactivatePackage(pack.name) for pack in @getActivePackages() + @unobserveDisabledPackages() deactivatePackage: (name) -> if pack = @getActivePackage(name) @@ -43,14 +90,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 +126,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 +148,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 +157,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..363852cda --- /dev/null +++ b/src/theme-package.coffee @@ -0,0 +1,30 @@ +AtomPackage = require './atom-package' +Package = require './package' + +### Internal: Loads and resolves packages. ### + +module.exports = +class ThemePackage extends AtomPackage + + getType: -> 'theme' + + getStylesheetType: -> 'theme' + + enable: -> + atom.config.pushAtKeyPath('core.themes', @metadata.name) + + 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()