diff --git a/coffeelint.json b/coffeelint.json index 9535d90ac..d8e19fc46 100644 --- a/coffeelint.json +++ b/coffeelint.json @@ -10,5 +10,8 @@ }, "no_interpolation_in_single_quotes": { "level": "error" + }, + "no_debugger": { + "level": "error" } } diff --git a/package.json b/package.json index bf7c1be48..4f5babcbd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "0.133.0", + "version": "0.134.0", "description": "A hackable text editor for the 21st Century.", "main": "./src/browser/main.js", "repository": { @@ -20,7 +20,7 @@ "atomShellVersion": "0.16.2", "dependencies": { "async": "0.2.6", - "atom-keymap": "^2.1.3", + "atom-keymap": "^2.2.0", "bootstrap": "git+https://github.com/atom/bootstrap.git#6af81906189f1747fd6c93479e3d998ebe041372", "clear-cut": "0.4.0", "coffee-script": "1.7.0", @@ -28,7 +28,7 @@ "delegato": "^1", "emissary": "^1.3.1", "event-kit": "0.7.2", - "first-mate": "^2.1.2", + "first-mate": "^2.2.0", "fs-plus": "^2.2.6", "fstream": "0.1.24", "fuzzaldrin": "^2.1", @@ -97,7 +97,7 @@ "open-on-github": "0.30.0", "package-generator": "0.31.0", "release-notes": "0.36.0", - "settings-view": "0.147.0", + "settings-view": "0.148.0", "snippets": "0.53.0", "spell-check": "0.42.0", "status-bar": "0.45.0", diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 450564ea2..666c76b3d 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -426,6 +426,7 @@ describe "Config", -> updatedHandler = null beforeEach -> + atom.config.setDefaults('foo', bar: 'def') atom.config.configDirPath = dotAtomPath atom.config.configFilePath = path.join(atom.config.configDirPath, "atom.config.cson") expect(fs.existsSync(atom.config.configDirPath)).toBeFalsy() @@ -447,6 +448,34 @@ describe "Config", -> expect(atom.config.get('foo.bar')).toBe 'quux' expect(atom.config.get('foo.baz')).toBe 'bar' + describe "when the config file changes to omit a setting with a default", -> + it "resets the setting back to the default", -> + fs.writeFileSync(atom.config.configFilePath, "foo: { baz: 'new'}") + waitsFor 'update event', -> updatedHandler.callCount > 0 + runs -> + expect(atom.config.get('foo.bar')).toBe 'def' + expect(atom.config.get('foo.baz')).toBe 'new' + + describe "when the config file changes to be empty", -> + beforeEach -> + fs.writeFileSync(atom.config.configFilePath, "") + waitsFor 'update event', -> updatedHandler.callCount > 0 + + it "resets all settings back to the defaults", -> + expect(updatedHandler.callCount).toBe 1 + expect(atom.config.get('foo.bar')).toBe 'def' + atom.config.set("hair", "blonde") # trigger a save + expect(atom.config.save).toHaveBeenCalled() + + describe "when the config file subsequently changes again to contain configuration", -> + beforeEach -> + updatedHandler.reset() + fs.writeFileSync(atom.config.configFilePath, "foo: bar: 'newVal'") + waitsFor 'update event', -> updatedHandler.callCount > 0 + + it "sets the setting to the value specified in the config file", -> + expect(atom.config.get('foo.bar')).toBe 'newVal' + describe "when the config file changes to contain invalid cson", -> beforeEach -> spyOn(console, 'error') diff --git a/spec/deserializer-manager-spec.coffee b/spec/deserializer-manager-spec.coffee index 3a2bf95e4..15c8a2648 100644 --- a/spec/deserializer-manager-spec.coffee +++ b/spec/deserializer-manager-spec.coffee @@ -1,33 +1,44 @@ DeserializerManager = require '../src/deserializer-manager' -describe ".deserialize(state)", -> - deserializer = null +describe "DeserializerManager", -> + manager = null class Foo @deserialize: ({name}) -> new Foo(name) constructor: (@name) -> beforeEach -> - deserializer = new DeserializerManager() - deserializer.add(Foo) + manager = new DeserializerManager - it "calls deserialize on the deserializer for the given state object, or returns undefined if one can't be found", -> - spyOn(console, 'warn') - object = deserializer.deserialize({ deserializer: 'Foo', name: 'Bar' }) - expect(object.name).toBe 'Bar' - expect(deserializer.deserialize({ deserializer: 'Bogus' })).toBeUndefined() + describe "::add(deserializer)", -> + it "returns a disposable that can be used to remove the manager", -> + disposable = manager.add(Foo) + expect(manager.deserialize({deserializer: 'Foo', name: 'Bar'})).toBeDefined() + disposable.dispose() + spyOn(console, 'warn') + expect(manager.deserialize({deserializer: 'Foo', name: 'Bar'})).toBeUndefined() - describe "when the deserializer has a version", -> + describe "::deserialize(state)", -> beforeEach -> - Foo.version = 2 + manager.add(Foo) - describe "when the deserialized state has a matching version", -> - it "attempts to deserialize the state", -> - object = deserializer.deserialize({ deserializer: 'Foo', version: 2, name: 'Bar' }) - expect(object.name).toBe 'Bar' + it "calls deserialize on the manager for the given state object, or returns undefined if one can't be found", -> + spyOn(console, 'warn') + object = manager.deserialize({deserializer: 'Foo', name: 'Bar'}) + expect(object.name).toBe 'Bar' + expect(manager.deserialize({deserializer: 'Bogus'})).toBeUndefined() - describe "when the deserialized state has a non-matching version", -> - it "returns undefined", -> - expect(deserializer.deserialize({ deserializer: 'Foo', version: 3, name: 'Bar' })).toBeUndefined() - expect(deserializer.deserialize({ deserializer: 'Foo', version: 1, name: 'Bar' })).toBeUndefined() - expect(deserializer.deserialize({ deserializer: 'Foo', name: 'Bar' })).toBeUndefined() + describe "when the manager has a version", -> + beforeEach -> + Foo.version = 2 + + describe "when the deserialized state has a matching version", -> + it "attempts to deserialize the state", -> + object = manager.deserialize({deserializer: 'Foo', version: 2, name: 'Bar'}) + expect(object.name).toBe 'Bar' + + describe "when the deserialized state has a non-matching version", -> + it "returns undefined", -> + expect(manager.deserialize({deserializer: 'Foo', version: 3, name: 'Bar'})).toBeUndefined() + expect(manager.deserialize({deserializer: 'Foo', version: 1, name: 'Bar'})).toBeUndefined() + expect(manager.deserialize({deserializer: 'Foo', name: 'Bar'})).toBeUndefined() diff --git a/spec/pane-container-view-spec.coffee b/spec/pane-container-view-spec.coffee index 8bbb75243..a48269a1a 100644 --- a/spec/pane-container-view-spec.coffee +++ b/spec/pane-container-view-spec.coffee @@ -5,11 +5,11 @@ PaneView = require '../src/pane-view' {$, View, $$} = require 'atom' describe "PaneContainerView", -> - [TestView, container, pane1, pane2, pane3] = [] + [TestView, container, pane1, pane2, pane3, deserializerDisposable] = [] beforeEach -> class TestView extends View - atom.deserializers.add(this) + deserializerDisposable = atom.deserializers.add(this) @deserialize: ({name}) -> new TestView(name) @content: -> @div tabindex: -1 initialize: (@name) -> @text(@name) @@ -25,7 +25,7 @@ describe "PaneContainerView", -> pane3 = pane2.splitDown(new TestView('3')) afterEach -> - atom.deserializers.remove(TestView) + deserializerDisposable.dispose() describe ".getActivePaneView()", -> it "returns the most-recently focused pane", -> diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee index 01dcda025..748de6c36 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-spec.coffee @@ -4,6 +4,8 @@ PaneAxis = require '../src/pane-axis' PaneContainer = require '../src/pane-container' describe "Pane", -> + deserializerDisposable = null + class Item extends Model @deserialize: ({name, uri}) -> new this(name, uri) constructor: (@name, @uri) -> @@ -13,10 +15,10 @@ describe "Pane", -> isEqual: (other) -> @name is other?.name beforeEach -> - atom.deserializers.add(Item) + deserializerDisposable = atom.deserializers.add(Item) afterEach -> - atom.deserializers.remove(Item) + deserializerDisposable.dispose() describe "construction", -> it "sets the active item to the first item", -> diff --git a/spec/pane-view-spec.coffee b/spec/pane-view-spec.coffee index 2a92ff3d5..5aa8c4ffc 100644 --- a/spec/pane-view-spec.coffee +++ b/spec/pane-view-spec.coffee @@ -7,7 +7,7 @@ path = require 'path' temp = require 'temp' describe "PaneView", -> - [container, containerModel, view1, view2, editor1, editor2, pane, paneModel] = [] + [container, containerModel, view1, view2, editor1, editor2, pane, paneModel, deserializerDisposable] = [] class TestView extends View @deserialize: ({id, text}) -> new TestView({id, text}) @@ -23,7 +23,7 @@ describe "PaneView", -> @emitter.on 'did-change-title', callback beforeEach -> - atom.deserializers.add(TestView) + deserializerDisposable = atom.deserializers.add(TestView) container = new PaneContainerView containerModel = container.model view1 = new TestView(id: 'view-1', text: 'View 1') @@ -40,7 +40,7 @@ describe "PaneView", -> paneModel.addItems([view1, editor1, view2, editor2]) afterEach -> - atom.deserializers.remove(TestView) + deserializerDisposable.dispose() describe "when the active pane item changes", -> it "hides all item views except the active one", -> diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee index 4a037d2f9..c7b64fec2 100644 --- a/spec/theme-manager-spec.coffee +++ b/spec/theme-manager-spec.coffee @@ -52,7 +52,7 @@ describe "ThemeManager", -> expect(themeManager.getEnabledThemeNames()).toEqual ['atom-dark-ui', 'atom-light-ui'] - describe "getImportPaths()", -> + describe "::getImportPaths()", -> it "returns the theme directories before the themes are loaded", -> atom.config.set('core.themes', ['theme-with-index-less', 'atom-dark-ui', 'atom-light-ui']) @@ -129,7 +129,7 @@ describe "ThemeManager", -> spyOn(console, 'warn') expect(-> atom.packages.activatePackage('a-theme-that-will-not-be-found')).toThrow() - describe "requireStylesheet(path)", -> + describe "::requireStylesheet(path)", -> it "synchronously loads css at the given path and installs a style tag for it in the head", -> themeManager.onDidChangeStylesheets stylesheetsChangedHandler = jasmine.createSpy("stylesheetsChangedHandler") themeManager.onDidAddStylesheet stylesheetAddedHandler = jasmine.createSpy("stylesheetAddedHandler") @@ -185,18 +185,17 @@ describe "ThemeManager", -> $('head style[id*="css.css"]').remove() $('head style[id*="sample.less"]').remove() - describe ".removeStylesheet(path)", -> - it "removes styling applied by given stylesheet path", -> + it "returns a disposable allowing styles applied by the given path to be removed", -> cssPath = require.resolve('./fixtures/css.css') expect($(document.body).css('font-weight')).not.toBe("bold") - themeManager.requireStylesheet(cssPath) + disposable = themeManager.requireStylesheet(cssPath) expect($(document.body).css('font-weight')).toBe("bold") themeManager.onDidRemoveStylesheet stylesheetRemovedHandler = jasmine.createSpy("stylesheetRemovedHandler") themeManager.onDidChangeStylesheets stylesheetsChangedHandler = jasmine.createSpy("stylesheetsChangedHandler") - themeManager.removeStylesheet(cssPath) + disposable.dispose() expect($(document.body).css('font-weight')).not.toBe("bold") diff --git a/src/config.coffee b/src/config.coffee index b2eb74f1e..f2063cdf1 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -533,7 +533,11 @@ class Config try userConfig = CSON.readFileSync(@configFilePath) - @setRecursive(null, userConfig) + @settings = {} # Reset to the defaults + if isPlainObject(userConfig) + @setRecursive(null, userConfig) + else + @emitter.emit 'did-change' @configFileHasErrors = false catch error @configFileHasErrors = true diff --git a/src/deserializer-manager.coffee b/src/deserializer-manager.coffee index 9abefc4c6..50becb31a 100644 --- a/src/deserializer-manager.coffee +++ b/src/deserializer-manager.coffee @@ -1,3 +1,6 @@ +{Disposable} = require 'event-kit' +Grim = require 'grim' + # Extended: Manages the deserializers used for serialized state # # An instance of this class is always available as the `atom.deserializers` @@ -24,14 +27,17 @@ class DeserializerManager # Public: Register the given class(es) as deserializers. # - # * `classes` One or more classes to register. - add: (classes...) -> - @deserializers[klass.name] = klass for klass in classes + # * `deserializers` One or more deserializers to register. A deserializer can + # be any object with a `.name` property and a `.deserialize()` method. A + # common approach is to register a *constructor* as the deserializer for its + # instances by adding a `.deserialize()` class method. + add: (deserializers...) -> + @deserializers[deserializer.name] = deserializer for deserializer in deserializers + new Disposable => + delete @deserializers[deserializer.name] for deserializer in deserializers - # Public: Remove the given class(es) as deserializers. - # - # * `classes` One or more classes to remove. remove: (classes...) -> + Grim.deprecate("Call .dispose() on the Disposable return from ::add instead") delete @deserializers[name] for {name} in classes # Public: Deserialize the state and params. diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee index 1aebe757b..9ea248a26 100644 --- a/src/theme-manager.coffee +++ b/src/theme-manager.coffee @@ -2,7 +2,7 @@ path = require 'path' _ = require 'underscore-plus' EmitterMixin = require('emissary').Emitter -{Emitter} = require 'event-kit' +{Emitter, Disposable} = require 'event-kit' {File} = require 'pathwatcher' fs = require 'fs-plus' Q = require 'q' @@ -182,16 +182,16 @@ class ThemeManager # * `stylesheetPath` A {String} path to the stylesheet that can be an absolute # path or a relative path that will be resolved against the load path. # - # Returns the absolute path to the required stylesheet. + # Returns a {Disposable} on which `.dispose()` can be called to remove the + # required stylesheet. requireStylesheet: (stylesheetPath, type='bundled') -> if fullPath = @resolveStylesheet(stylesheetPath) content = @loadStylesheet(fullPath) @applyStylesheet(fullPath, content, type) + new Disposable => @removeStylesheet(fullPath) else throw new Error("Could not find a file at path '#{stylesheetPath}'") - fullPath - unwatchUserStylesheet: -> @userStylesheetFile?.off() @userStylesheetFile = null diff --git a/src/workspace.coffee b/src/workspace.coffee index 65bf5895f..1d890c217 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -5,7 +5,8 @@ _ = require 'underscore-plus' Q = require 'q' Serializable = require 'serializable' Delegator = require 'delegato' -{Emitter} = require 'event-kit' +{Emitter, Disposable} = require 'event-kit' +Grim = require 'grim' TextEditor = require './text-editor' PaneContainer = require './pane-container' Pane = require './pane' @@ -363,11 +364,16 @@ class Workspace extends Model # ``` # # * `opener` A {Function} to be called when a path is being opened. + # + # Returns a {Disposable} on which `.dispose()` can be called to remove the + # opener. registerOpener: (opener) -> @openers.push(opener) + new Disposable => _.remove(@openers, opener) # Unregister an opener registered with {::registerOpener}. unregisterOpener: (opener) -> + Grim.deprecate("Call .dispose() on the Disposable returned from ::registerOpener instead") _.remove(@openers, opener) getOpeners: ->