From bbeb0b5919a49908107150aec5e8cd15ee6014ef Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 29 Sep 2014 10:34:50 -0600 Subject: [PATCH 001/145] Return disposables from MenuManager which can be used to remove menus --- spec/menu-manager-spec.coffee | 37 +++++++++++++++++++++++++++++++++++ src/menu-manager.coffee | 30 ++++++++++++++++++++++++---- 2 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 spec/menu-manager-spec.coffee diff --git a/spec/menu-manager-spec.coffee b/spec/menu-manager-spec.coffee new file mode 100644 index 000000000..a018d3f84 --- /dev/null +++ b/spec/menu-manager-spec.coffee @@ -0,0 +1,37 @@ +describe "MenuManager", -> + describe "::add(items)", -> + it "can add new menus that can be removed with the returned disposable", -> + originalItemCount = atom.menu.template.length + disposable = atom.menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}] + expect(atom.menu.template[originalItemCount]).toEqual {label: "A", submenu: [{label: "B", command: "b"}]} + disposable.dispose() + expect(atom.menu.template.length).toBe originalItemCount + + it "can submenu items to existing menus that can be removed with the returned disposable", -> + originalItemCount = atom.menu.template.length + disposable1 = atom.menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}] + disposable2 = atom.menu.add [{label: "A", submenu: [{label: "C", submenu: [{label: "D", command: 'd'}]}]}] + disposable3 = atom.menu.add [{label: "A", submenu: [{label: "C", submenu: [{label: "E", command: 'e'}]}]}] + + expect(atom.menu.template[originalItemCount]).toEqual { + label: "A", + submenu: [ + {label: "B", command: "b"}, + {label: "C", submenu: [{label: 'D', command: 'd'}, {label: 'E', command: 'e'}]} + ] + } + + disposable3.dispose() + expect(atom.menu.template[originalItemCount]).toEqual { + label: "A", + submenu: [ + {label: "B", command: "b"}, + {label: "C", submenu: [{label: 'D', command: 'd'}]} + ] + } + + disposable2.dispose() + expect(atom.menu.template[originalItemCount]).toEqual {label: "A", submenu: [{label: "B", command: "b"}]} + + disposable1.dispose() + expect(atom.menu.template.length).toBe originalItemCount diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee index 89462ec7a..f03b4ce73 100644 --- a/src/menu-manager.coffee +++ b/src/menu-manager.coffee @@ -4,6 +4,7 @@ _ = require 'underscore-plus' ipc = require 'ipc' CSON = require 'season' fs = require 'fs-plus' +{Disposable} = require 'event-kit' # Extended: Provides a registry for menu items that you'd like to appear in the # application menu. @@ -37,7 +38,11 @@ class MenuManager add: (items) -> @merge(@template, item) for item in items @update() - undefined + new Disposable => @remove(items) + + remove: (items) -> + @unmerge(@template, item) for item in items + @update() # Should the binding for the given selector be included in the menu # commands. @@ -96,11 +101,28 @@ class MenuManager # appended to the bottom of existing menus where possible. merge: (menu, item) -> item = _.deepClone(item) + matchingItem = @findMatchingItem(menu, item) - if item.submenu? and match = _.find(menu, ({label, submenu}) => submenu? and label and @normalizeLabel(label) is @normalizeLabel(item.label)) - @merge(match.submenu, i) for i in item.submenu + if matchingItem? and item.submenu? + @merge(matchingItem.submenu, submenuItem) for submenuItem in item.submenu else - menu.push(item) unless _.find(menu, ({label}) => label and @normalizeLabel(label) is @normalizeLabel(item.label)) + menu.push(item) + + unmerge: (menu, item) -> + if matchingItem = @findMatchingItem(menu, item) + if item.submenu? + @unmerge(matchingItem.submenu, submenuItem) for submenuItem in item.submenu + + unless matchingItem.submenu?.length > 0 + menu.splice(menu.indexOf(matchingItem), 1) + + # find an existing menu item matching the given item + findMatchingItem: (menu, {label, submenu}) -> + debugger unless menu? + for item in menu + if @normalizeLabel(item.label) is @normalizeLabel(label) and item.submenu? is submenu? + return item + null # OSX can't handle displaying accelerators for multiple keystrokes. # If they are sent across, it will stop processing accelerators for the rest From 81a7f65832cca3b26d4ede1f1baf3b936de8d25b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 29 Sep 2014 10:37:19 -0600 Subject: [PATCH 002/145] :pencil: Update docs --- src/menu-manager.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee index f03b4ce73..cf51cd2c5 100644 --- a/src/menu-manager.coffee +++ b/src/menu-manager.coffee @@ -35,6 +35,9 @@ class MenuManager # * `submenu` An optional {Array} of sub menu items. # * `command` An optional {String} command to trigger when the item is # clicked. + # + # Returns a {Disposable} on which `.dispose()` can be called to remove the + # added menu items. add: (items) -> @merge(@template, item) for item in items @update() From c058b44a1b3c0902dcc854832e73c0e8582bc369 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 29 Sep 2014 11:11:10 -0600 Subject: [PATCH 003/145] :lipstick: spec description --- spec/menu-manager-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/menu-manager-spec.coffee b/spec/menu-manager-spec.coffee index a018d3f84..2344a995c 100644 --- a/spec/menu-manager-spec.coffee +++ b/spec/menu-manager-spec.coffee @@ -7,7 +7,7 @@ describe "MenuManager", -> disposable.dispose() expect(atom.menu.template.length).toBe originalItemCount - it "can submenu items to existing menus that can be removed with the returned disposable", -> + it "can add submenu items to existing menus that can be removed with the returned disposable", -> originalItemCount = atom.menu.template.length disposable1 = atom.menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}] disposable2 = atom.menu.add [{label: "A", submenu: [{label: "C", submenu: [{label: "D", command: 'd'}]}]}] From 2f93032a37c16e53f0c35dd8bacf3179f4d0d7cf Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 29 Sep 2014 11:11:36 -0600 Subject: [PATCH 004/145] =?UTF-8?q?Don=E2=80=99t=20add=20duplicate=20items?= =?UTF-8?q?=20to=20the=20same=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/menu-manager-spec.coffee | 6 ++++++ src/menu-manager.coffee | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/spec/menu-manager-spec.coffee b/spec/menu-manager-spec.coffee index 2344a995c..8c03328de 100644 --- a/spec/menu-manager-spec.coffee +++ b/spec/menu-manager-spec.coffee @@ -35,3 +35,9 @@ describe "MenuManager", -> disposable1.dispose() expect(atom.menu.template.length).toBe originalItemCount + + it "does not add duplicate labels to the same menu", -> + originalItemCount = atom.menu.template.length + atom.menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}] + atom.menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}] + expect(atom.menu.template[originalItemCount]).toEqual {label: "A", submenu: [{label: "B", command: "b"}]} diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee index cf51cd2c5..94d6fe5d3 100644 --- a/src/menu-manager.coffee +++ b/src/menu-manager.coffee @@ -106,8 +106,9 @@ class MenuManager item = _.deepClone(item) matchingItem = @findMatchingItem(menu, item) - if matchingItem? and item.submenu? - @merge(matchingItem.submenu, submenuItem) for submenuItem in item.submenu + if matchingItem? + if item.submenu? + @merge(matchingItem.submenu, submenuItem) for submenuItem in item.submenu else menu.push(item) From f6f891fa141cb22038683d8e104258483be42881 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 29 Sep 2014 11:45:01 -0600 Subject: [PATCH 005/145] Construct test instance of MenuManager in spec --- spec/menu-manager-spec.coffee | 41 ++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/spec/menu-manager-spec.coffee b/spec/menu-manager-spec.coffee index 8c03328de..d92c921c4 100644 --- a/spec/menu-manager-spec.coffee +++ b/spec/menu-manager-spec.coffee @@ -1,43 +1,48 @@ +MenuManager = require '../src/menu-manager' + describe "MenuManager", -> + menu = null + + beforeEach -> + menu = new MenuManager(resourcePath: atom.getLoadSettings().resourcePath) + describe "::add(items)", -> it "can add new menus that can be removed with the returned disposable", -> - originalItemCount = atom.menu.template.length - disposable = atom.menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}] - expect(atom.menu.template[originalItemCount]).toEqual {label: "A", submenu: [{label: "B", command: "b"}]} + disposable = menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}] + expect(menu.template).toEqual [{label: "A", submenu: [{label: "B", command: "b"}]}] disposable.dispose() - expect(atom.menu.template.length).toBe originalItemCount + expect(menu.template).toEqual [] it "can add submenu items to existing menus that can be removed with the returned disposable", -> - originalItemCount = atom.menu.template.length - disposable1 = atom.menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}] - disposable2 = atom.menu.add [{label: "A", submenu: [{label: "C", submenu: [{label: "D", command: 'd'}]}]}] - disposable3 = atom.menu.add [{label: "A", submenu: [{label: "C", submenu: [{label: "E", command: 'e'}]}]}] + disposable1 = menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}] + disposable2 = menu.add [{label: "A", submenu: [{label: "C", submenu: [{label: "D", command: 'd'}]}]}] + disposable3 = menu.add [{label: "A", submenu: [{label: "C", submenu: [{label: "E", command: 'e'}]}]}] - expect(atom.menu.template[originalItemCount]).toEqual { + expect(menu.template).toEqual [{ label: "A", submenu: [ {label: "B", command: "b"}, {label: "C", submenu: [{label: 'D', command: 'd'}, {label: 'E', command: 'e'}]} ] - } + }] disposable3.dispose() - expect(atom.menu.template[originalItemCount]).toEqual { + expect(menu.template).toEqual [{ label: "A", submenu: [ {label: "B", command: "b"}, {label: "C", submenu: [{label: 'D', command: 'd'}]} ] - } + }] disposable2.dispose() - expect(atom.menu.template[originalItemCount]).toEqual {label: "A", submenu: [{label: "B", command: "b"}]} + expect(menu.template).toEqual [{label: "A", submenu: [{label: "B", command: "b"}]}] disposable1.dispose() - expect(atom.menu.template.length).toBe originalItemCount + expect(menu.template).toEqual [] it "does not add duplicate labels to the same menu", -> - originalItemCount = atom.menu.template.length - atom.menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}] - atom.menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}] - expect(atom.menu.template[originalItemCount]).toEqual {label: "A", submenu: [{label: "B", command: "b"}]} + originalItemCount = menu.template.length + menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}] + menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}] + expect(menu.template[originalItemCount]).toEqual {label: "A", submenu: [{label: "B", command: "b"}]} From c56babec8dc86ca3764b7df02c3b22f01ff2fea1 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 29 Sep 2014 09:27:57 -0700 Subject: [PATCH 006/145] Add split commands to editor context menu Refs atom/tabs#85 --- menus/darwin.cson | 7 +++++++ menus/linux.cson | 7 +++++++ menus/win32.cson | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/menus/darwin.cson b/menus/darwin.cson index b892c56cc..e73340a45 100644 --- a/menus/darwin.cson +++ b/menus/darwin.cson @@ -199,8 +199,15 @@ '.overlayer': 'Undo': 'core:undo' 'Redo': 'core:redo' + 'separator1': '-' 'Cut': 'core:cut' 'Copy': 'core:copy' 'Paste': 'core:paste' 'Delete': 'core:delete' 'Select All': 'core:select-all' + 'separator2': '-' + 'Split Up': 'pane:split-up' + 'Split Down': 'pane:split-down' + 'Split Left': 'pane:split-left' + 'Split Right': 'pane:split-right' + 'separator3': '-' diff --git a/menus/linux.cson b/menus/linux.cson index ba87732dd..756d306d3 100644 --- a/menus/linux.cson +++ b/menus/linux.cson @@ -156,8 +156,15 @@ '.overlayer': 'Undo': 'core:undo' 'Redo': 'core:redo' + 'separator1': '-' 'Cut': 'core:cut' 'Copy': 'core:copy' 'Paste': 'core:paste' 'Delete': 'core:delete' 'Select All': 'core:select-all' + 'separator2': '-' + 'Split Up': 'pane:split-up' + 'Split Down': 'pane:split-down' + 'Split Left': 'pane:split-left' + 'Split Right': 'pane:split-right' + 'separator3': '-' diff --git a/menus/win32.cson b/menus/win32.cson index 588e21978..96dbc2537 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -174,8 +174,15 @@ '.overlayer': 'Undo': 'core:undo' 'Redo': 'core:redo' + 'separator1': '-' 'Cut': 'core:cut' 'Copy': 'core:copy' 'Paste': 'core:paste' 'Delete': 'core:delete' 'Select All': 'core:select-all' + 'separator2': '-' + 'Split Up': 'pane:split-up' + 'Split Down': 'pane:split-down' + 'Split Left': 'pane:split-left' + 'Split Right': 'pane:split-right' + 'separator3': '-' From 0499ee65a46216affa1fa82516afde9ec470879b Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 29 Sep 2014 11:07:40 -0700 Subject: [PATCH 007/145] Upgrade to tabs@0.54 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cbddc998d..55d4d4e41 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "status-bar": "0.45.0", "styleguide": "0.30.0", "symbols-view": "0.66.0", - "tabs": "0.53.0", + "tabs": "0.54.0", "timecop": "0.22.0", "tree-view": "0.127.0", "update-package-dependencies": "0.6.0", From e7ad9ae15acacc6b3787139ecd6c81efd9385e79 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 29 Sep 2014 13:22:55 -0700 Subject: [PATCH 008/145] Add --deep codesign option for 10.9.5 --- build/tasks/codesign-task.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/tasks/codesign-task.coffee b/build/tasks/codesign-task.coffee index d24664c99..f8fab2864 100644 --- a/build/tasks/codesign-task.coffee +++ b/build/tasks/codesign-task.coffee @@ -26,7 +26,7 @@ module.exports = (grunt) -> switch process.platform when 'darwin' cmd = 'codesign' - args = ['-f', '-v', '-s', 'Developer ID Application: GitHub', grunt.config.get('atom.shellAppDir')] + args = ['--deep', '-f', '-v', '-s', 'Developer ID Application: GitHub', grunt.config.get('atom.shellAppDir')] spawn {cmd, args}, (error) -> callback(error) when 'win32' spawn {cmd: 'taskkill', args: ['/F', '/IM', 'atom.exe']}, -> From ea75636e4478a2e3737e5fe863fefd8323328a84 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 29 Sep 2014 13:26:36 -0700 Subject: [PATCH 009/145] Use long options --- build/tasks/codesign-task.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/tasks/codesign-task.coffee b/build/tasks/codesign-task.coffee index f8fab2864..82ed5701c 100644 --- a/build/tasks/codesign-task.coffee +++ b/build/tasks/codesign-task.coffee @@ -26,7 +26,7 @@ module.exports = (grunt) -> switch process.platform when 'darwin' cmd = 'codesign' - args = ['--deep', '-f', '-v', '-s', 'Developer ID Application: GitHub', grunt.config.get('atom.shellAppDir')] + args = ['--deep', '--force', '--verbose', '--sign', 'Developer ID Application: GitHub', grunt.config.get('atom.shellAppDir')] spawn {cmd, args}, (error) -> callback(error) when 'win32' spawn {cmd: 'taskkill', args: ['/F', '/IM', 'atom.exe']}, -> From 69f24a157a2e843b620e51e270b8d36c67533ec0 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 29 Sep 2014 13:41:00 -0700 Subject: [PATCH 010/145] Upgrade to language-coffee-script@0.35 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 55d4d4e41..e90d545d6 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "whitespace": "0.25.0", "wrap-guide": "0.22.0", "language-c": "0.28.0", - "language-coffee-script": "0.34.0", + "language-coffee-script": "0.35.0", "language-css": "0.17.0", "language-gfm": "0.50.0", "language-git": "0.9.0", From 9e46ab1b486f6cb994d2bd2c428a5ca471ce2947 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 23 Sep 2014 11:21:21 -0700 Subject: [PATCH 011/145] Reorder config methods for easier digestion --- src/config.coffee | 255 ++++++++++++++++++++++++---------------------- 1 file changed, 134 insertions(+), 121 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index bdf2dde42..0d2885386 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -34,71 +34,9 @@ class Config @configFilePath = fs.resolve(@configDirPath, 'config', ['json', 'cson']) @configFilePath ?= path.join(@configDirPath, 'config.cson') - initializeConfigDirectory: (done) -> - return if fs.existsSync(@configDirPath) - - fs.makeTreeSync(@configDirPath) - - queue = async.queue ({sourcePath, destinationPath}, callback) -> - fs.copy(sourcePath, destinationPath, callback) - queue.drain = done - - templateConfigDirPath = fs.resolve(@resourcePath, 'dot-atom') - onConfigDirFile = (sourcePath) => - relativePath = sourcePath.substring(templateConfigDirPath.length + 1) - destinationPath = path.join(@configDirPath, relativePath) - queue.push({sourcePath, destinationPath}) - fs.traverseTree(templateConfigDirPath, onConfigDirFile, (path) -> true) - - load: -> - @initializeConfigDirectory() - @loadUserConfig() - @observeUserConfig() - - loadUserConfig: -> - unless fs.existsSync(@configFilePath) - fs.makeTreeSync(path.dirname(@configFilePath)) - CSON.writeFileSync(@configFilePath, {}) - - try - userConfig = CSON.readFileSync(@configFilePath) - _.extend(@settings, userConfig) - @configFileHasErrors = false - @emit 'updated' - catch error - @configFileHasErrors = true - console.error "Failed to load user config '#{@configFilePath}'", error.message - console.error error.stack - - observeUserConfig: -> - try - @watchSubscription ?= pathWatcher.watch @configFilePath, (eventType) => - @loadUserConfig() if eventType is 'change' and @watchSubscription? - catch error - console.error "Failed to watch user config '#{@configFilePath}'", error.message - console.error error.stack - - unobserveUserConfig: -> - @watchSubscription?.close() - @watchSubscription = null - - setDefaults: (keyPath, defaults) -> - keys = keyPath.split('.') - hash = @defaultSettings - for key in keys - hash[key] ?= {} - hash = hash[key] - - _.extend hash, defaults - @emit 'updated' - - # Extended: Get the {String} path to the config file being used. - getUserConfigPath: -> - @configFilePath - - # Extended: Returns a new {Object} containing all of settings and defaults. - getSettings: -> - _.deepExtend(@settings, @defaultSettings) + ### + Section: get / set + ### # Essential: Retrieves the setting for the given key. # @@ -121,26 +59,6 @@ class Config value - # Extended: Retrieves the setting for the given key as an integer. - # - # * `keyPath` The {String} name of the key to retrieve - # - # Returns the value from Atom's default settings, the user's configuration - # file, or `NaN` if the key doesn't exist in either. - getInt: (keyPath) -> - parseInt(@get(keyPath)) - - # Extended: Retrieves the setting for the given key as a positive integer. - # - # * `keyPath` The {String} name of the key to retrieve - # * `defaultValue` The integer {Number} to fall back to if the value isn't - # positive, defaults to 0. - # - # Returns the value from Atom's default settings, the user's configuration - # file, or `defaultValue` if the key value isn't greater than zero. - getPositiveInt: (keyPath, defaultValue=0) -> - Math.max(@getInt(keyPath), 0) or defaultValue - # Essential: Sets the value for a configuration setting. # # This value is stored in Atom's internal configuration file. @@ -157,16 +75,44 @@ class Config @update() value - # Extended: Toggle the value at the key path. + # Essential: Add a listener for changes to a given key path. # - # The new value will be `true` if the value is currently falsy and will be - # `false` if the value is currently truthy. + # * `keyPath` The {String} name of the key to observe + # * `options` An optional {Object} containing the `callNow` key. + # * `callback` The {Function} to call when the value of the key changes. + # The first argument will be the new value of the key and the + #   second argument will be an {Object} with a `previous` property + # that is the prior value of the key. # - # * `keyPath` The {String} name of the key. - # - # Returns the new value. - toggle: (keyPath) -> - @set(keyPath, !@get(keyPath)) + # Returns an {Object} with the following keys: + # * `off` A {Function} that unobserves the `keyPath` when called. + observe: (keyPath, options={}, callback) -> + if _.isFunction(options) + callback = options + options = {} + + value = @get(keyPath) + previousValue = _.clone(value) + updateCallback = => + value = @get(keyPath) + unless _.isEqual(value, previousValue) + previous = previousValue + previousValue = _.clone(value) + callback(value, {previous}) + + eventName = "updated.#{keyPath.replace(/\./, '-')}" + subscription = @on eventName, updateCallback + callback(value) if options.callNow ? true + subscription + + # Extended: Get the {String} path to the config file being used. + getUserConfigPath: -> + @configFilePath + + # Extended: Returns a new {Object} containing all of settings and defaults. + getSettings: -> + _.deepExtend(@settings, @defaultSettings) + # Extended: Restore the key path to its default value. # @@ -230,42 +176,109 @@ class Config @set(keyPath, arrayValue) result - # Essential: Add a listener for changes to a given key path. - # - # * `keyPath` The {String} name of the key to observe - # * `options` An optional {Object} containing the `callNow` key. - # * `callback` The {Function} to call when the value of the key changes. - # The first argument will be the new value of the key and the - #   second argument will be an {Object} with a `previous` property - # that is the prior value of the key. - # - # Returns an {Object} with the following keys: - # * `off` A {Function} that unobserves the `keyPath` when called. - observe: (keyPath, options={}, callback) -> - if _.isFunction(options) - callback = options - options = {} + ### + Section: To Deprecate + ### - value = @get(keyPath) - previousValue = _.clone(value) - updateCallback = => - value = @get(keyPath) - unless _.isEqual(value, previousValue) - previous = previousValue - previousValue = _.clone(value) - callback(value, {previous}) + # Retrieves the setting for the given key as an integer. + # + # * `keyPath` The {String} name of the key to retrieve + # + # Returns the value from Atom's default settings, the user's configuration + # file, or `NaN` if the key doesn't exist in either. + getInt: (keyPath) -> + parseInt(@get(keyPath)) - eventName = "updated.#{keyPath.replace(/\./, '-')}" - subscription = @on eventName, updateCallback - callback(value) if options.callNow ? true - subscription + # Retrieves the setting for the given key as a positive integer. + # + # * `keyPath` The {String} name of the key to retrieve + # * `defaultValue` The integer {Number} to fall back to if the value isn't + # positive, defaults to 0. + # + # Returns the value from Atom's default settings, the user's configuration + # file, or `defaultValue` if the key value isn't greater than zero. + getPositiveInt: (keyPath, defaultValue=0) -> + Math.max(@getInt(keyPath), 0) or defaultValue + + # Toggle the value at the key path. + # + # The new value will be `true` if the value is currently falsy and will be + # `false` if the value is currently truthy. + # + # * `keyPath` The {String} name of the key. + # + # Returns the new value. + toggle: (keyPath) -> + @set(keyPath, !@get(keyPath)) # Unobserve all callbacks on a given key. - # # * `keyPath` The {String} name of the key to unobserve. + # unobserve: (keyPath) -> @off("updated.#{keyPath.replace(/\./, '-')}") + ### + Section: Private + ### + + initializeConfigDirectory: (done) -> + return if fs.existsSync(@configDirPath) + + fs.makeTreeSync(@configDirPath) + + queue = async.queue ({sourcePath, destinationPath}, callback) -> + fs.copy(sourcePath, destinationPath, callback) + queue.drain = done + + templateConfigDirPath = fs.resolve(@resourcePath, 'dot-atom') + onConfigDirFile = (sourcePath) => + relativePath = sourcePath.substring(templateConfigDirPath.length + 1) + destinationPath = path.join(@configDirPath, relativePath) + queue.push({sourcePath, destinationPath}) + fs.traverseTree(templateConfigDirPath, onConfigDirFile, (path) -> true) + + load: -> + @initializeConfigDirectory() + @loadUserConfig() + @observeUserConfig() + + loadUserConfig: -> + unless fs.existsSync(@configFilePath) + fs.makeTreeSync(path.dirname(@configFilePath)) + CSON.writeFileSync(@configFilePath, {}) + + try + userConfig = CSON.readFileSync(@configFilePath) + _.extend(@settings, userConfig) + @configFileHasErrors = false + @emit 'updated' + catch error + @configFileHasErrors = true + console.error "Failed to load user config '#{@configFilePath}'", error.message + console.error error.stack + + observeUserConfig: -> + try + @watchSubscription ?= pathWatcher.watch @configFilePath, (eventType) => + @loadUserConfig() if eventType is 'change' and @watchSubscription? + catch error + console.error "Failed to watch user config '#{@configFilePath}'", error.message + console.error error.stack + + unobserveUserConfig: -> + @watchSubscription?.close() + @watchSubscription = null + + setDefaults: (keyPath, defaults) -> + keys = keyPath.split('.') + hash = @defaultSettings + for key in keys + hash[key] ?= {} + hash = hash[key] + + _.extend hash, defaults + @emit 'updated' + update: -> return if @configFileHasErrors @save() From a79c01577440e9b46d381ab129c134b184615c64 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 23 Sep 2014 11:58:20 -0700 Subject: [PATCH 012/145] Update ::observe and add ::onDidChange --- spec/config-spec.coffee | 20 +++++++++ src/config.coffee | 91 ++++++++++++++++++++++++++--------------- 2 files changed, 78 insertions(+), 33 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 117bfe2fd..6b4852e26 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -231,6 +231,26 @@ describe "Config", -> atom.config.setDefaults("foo.bar.baz", a: 2) expect(updatedCallback.callCount).toBe 1 + describe ".onDidChange(keyPath)", -> + [observeHandler, observeSubscription] = [] + + beforeEach -> + observeHandler = jasmine.createSpy("observeHandler") + atom.config.set("foo.bar.baz", "value 1") + observeSubscription = atom.config.onDidChange "foo.bar.baz", observeHandler + + it "does not fire the given callback with the current value at the keypath", -> + expect(observeHandler).not.toHaveBeenCalledWith("value 1") + + it "fires the callback every time the observed value changes", -> + observeHandler.reset() # clear the initial call + atom.config.set('foo.bar.baz', "value 2") + expect(observeHandler).toHaveBeenCalledWith("value 2", {previous: 'value 1'}) + observeHandler.reset() + + atom.config.set('foo.bar.baz', "value 1") + expect(observeHandler).toHaveBeenCalledWith("value 1", {previous: 'value 2'}) + describe ".observe(keyPath)", -> [observeHandler, observeSubscription] = [] diff --git a/src/config.coffee b/src/config.coffee index 0d2885386..ae12cf8aa 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -1,10 +1,12 @@ _ = require 'underscore-plus' fs = require 'fs-plus' -{Emitter} = require 'emissary' +EmitterMixin = require('emissary').Emitter +{Emitter} = require 'event-kit' CSON = require 'season' path = require 'path' async = require 'async' pathWatcher = require 'pathwatcher' +{deprecate} = require 'grim' # Essential: Used to access all of Atom's configuration details. # @@ -24,16 +26,67 @@ pathWatcher = require 'pathwatcher' # ``` module.exports = class Config - Emitter.includeInto(this) + EmitterMixin.includeInto(this) # Created during initialization, available as `atom.config` constructor: ({@configDirPath, @resourcePath}={}) -> + @emitter = new Emitter @defaultSettings = {} @settings = {} @configFileHasErrors = false @configFilePath = fs.resolve(@configDirPath, 'config', ['json', 'cson']) @configFilePath ?= path.join(@configDirPath, 'config.cson') + ### + Section: Config Subscription + ### + + # Essential: Add a listener for changes to a given key path. + # + # * `keyPath` The {String} name of the key to observe + # * `callback` The {Function} to call when the value of the key changes. + # The first argument will be the new value of the key and the + #   second argument will be an {Object} with a `previous` property + # that is the prior value of the key. + # + # Returns a {Disposable} with the following keys on which you can call + # `.dispose()` to unsubscribe. + onDidChange: (keyPath, callback) -> + value = @get(keyPath) + previousValue = _.clone(value) + updateCallback = => + value = @get(keyPath) + unless _.isEqual(value, previousValue) + previous = previousValue + previousValue = _.clone(value) + callback(value, {previous}) + + @emitter.on 'did-change', updateCallback + + # Essential: Add a listener for changes to a given key path. This is different + # than {::onDidChange} in that it will immediately call your callback with the + # current value of the config entry. + # + # * `keyPath` The {String} name of the key to observe + # * `callback` The {Function} to call when the value of the key changes. + # The first argument will be the new value of the key and the + #   second argument will be an {Object} with a `previous` property + # that is the prior value of the key. + # + # Returns a {Disposable} with the following keys on which you can call + # `.dispose()` to unsubscribe. + observe: (keyPath, options={}, callback) -> + if _.isFunction(options) + callback = options + options = {} + else + message = '' + message = '`callNow` as been set to false. Use ::onDidChange instead.' if options.callNow == false + deprecate 'Config::observe no longer supports options. #{message}' + + callback(_.clone(@get(keyPath))) unless options.callNow == false + @onDidChange(keyPath, callback) + ### Section: get / set ### @@ -75,36 +128,6 @@ class Config @update() value - # Essential: Add a listener for changes to a given key path. - # - # * `keyPath` The {String} name of the key to observe - # * `options` An optional {Object} containing the `callNow` key. - # * `callback` The {Function} to call when the value of the key changes. - # The first argument will be the new value of the key and the - #   second argument will be an {Object} with a `previous` property - # that is the prior value of the key. - # - # Returns an {Object} with the following keys: - # * `off` A {Function} that unobserves the `keyPath` when called. - observe: (keyPath, options={}, callback) -> - if _.isFunction(options) - callback = options - options = {} - - value = @get(keyPath) - previousValue = _.clone(value) - updateCallback = => - value = @get(keyPath) - unless _.isEqual(value, previousValue) - previous = previousValue - previousValue = _.clone(value) - callback(value, {previous}) - - eventName = "updated.#{keyPath.replace(/\./, '-')}" - subscription = @on eventName, updateCallback - callback(value) if options.callNow ? true - subscription - # Extended: Get the {String} path to the config file being used. getUserConfigPath: -> @configFilePath @@ -113,7 +136,6 @@ class Config getSettings: -> _.deepExtend(@settings, @defaultSettings) - # Extended: Restore the key path to its default value. # # * `keyPath` The {String} name of the key. @@ -252,6 +274,7 @@ class Config _.extend(@settings, userConfig) @configFileHasErrors = false @emit 'updated' + @emitter.emit 'did-change' catch error @configFileHasErrors = true console.error "Failed to load user config '#{@configFilePath}'", error.message @@ -278,11 +301,13 @@ class Config _.extend hash, defaults @emit 'updated' + @emitter.emit 'did-change' update: -> return if @configFileHasErrors @save() @emit 'updated' + @emitter.emit 'did-change' save: -> CSON.writeFileSync(@configFilePath, @settings) From a84dd69f55711da04b887ee1b69a47139d25f71e Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 23 Sep 2014 13:32:14 -0700 Subject: [PATCH 013/145] Deprecate unused / unnecessary methods --- src/config.coffee | 81 +++++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index ae12cf8aa..b0e6d64d3 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -162,42 +162,6 @@ class Config isDefault: (keyPath) -> not _.valueForKeyPath(@settings, keyPath)? - # Extended: Push the value to the array at the key path. - # - # * `keyPath` The {String} key path. - # * `value` The value to push to the array. - # - # Returns the new array length {Number} of the setting. - pushAtKeyPath: (keyPath, value) -> - arrayValue = @get(keyPath) ? [] - result = arrayValue.push(value) - @set(keyPath, arrayValue) - result - - # Extended: Add the value to the beginning of the array at the key path. - # - # * `keyPath` The {String} key path. - # * `value` The value to shift onto the array. - # - # Returns the new array length {Number} of the setting. - unshiftAtKeyPath: (keyPath, value) -> - arrayValue = @get(keyPath) ? [] - result = arrayValue.unshift(value) - @set(keyPath, arrayValue) - result - - # Public: Remove the value from the array at the key path. - # - # * `keyPath` The {String} key path. - # * `value` The value to remove from the array. - # - # Returns the new array value of the setting. - removeAtKeyPath: (keyPath, value) -> - arrayValue = @get(keyPath) ? [] - result = _.remove(arrayValue, value) - @set(keyPath, arrayValue) - result - ### Section: To Deprecate ### @@ -233,11 +197,54 @@ class Config toggle: (keyPath) -> @set(keyPath, !@get(keyPath)) + ### + Section: Deprecated + ### + # Unobserve all callbacks on a given key. # * `keyPath` The {String} name of the key to unobserve. # unobserve: (keyPath) -> - @off("updated.#{keyPath.replace(/\./, '-')}") + deprecate 'Config::unobserve no longer does anything. Call `.dispose()` on the object returned by Config::observe instead.' + + # Push the value to the array at the key path. + # + # * `keyPath` The {String} key path. + # * `value` The value to push to the array. + # + # Returns the new array length {Number} of the setting. + pushAtKeyPath: (keyPath, value) -> + deprecate 'Please remove from your code. Config::pushAtKeyPath is going away. Please push the value onto the array, and call Config::set' + arrayValue = @get(keyPath) ? [] + result = arrayValue.push(value) + @set(keyPath, arrayValue) + result + + # Add the value to the beginning of the array at the key path. + # + # * `keyPath` The {String} key path. + # * `value` The value to shift onto the array. + # + # Returns the new array length {Number} of the setting. + unshiftAtKeyPath: (keyPath, value) -> + deprecate 'Please remove from your code. Config::unshiftAtKeyPath is going away. Please unshift the value onto the array, and call Config::set' + arrayValue = @get(keyPath) ? [] + result = arrayValue.unshift(value) + @set(keyPath, arrayValue) + result + + # Remove the value from the array at the key path. + # + # * `keyPath` The {String} key path. + # * `value` The value to remove from the array. + # + # Returns the new array value of the setting. + removeAtKeyPath: (keyPath, value) -> + deprecate 'Please remove from your code. Config::removeAtKeyPath is going away. Please remove the value from the array, and call Config::set' + arrayValue = @get(keyPath) ? [] + result = _.remove(arrayValue, value) + @set(keyPath, arrayValue) + result ### Section: Private From 02e87555f41a616afa758abcf790386f908a3e75 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 23 Sep 2014 15:02:22 -0700 Subject: [PATCH 014/145] Handle schema loading --- spec/config-spec.coffee | 87 +++++++++++++++++++++++++++++++++++++++++ src/config.coffee | 41 +++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 6b4852e26..45c3f4104 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -399,3 +399,90 @@ describe "Config", -> it "updates the config data and resumes saving", -> atom.config.set("hair", "blonde") expect(atom.config.save).toHaveBeenCalled() + + describe "when there is a schema specified", -> + schema = null + + describe '.setSchema(keyPath, schema)', -> + it 'sets defaults specified by the schema', -> + schema = + type: 'object' + properties: + anInt: + type: 'integer' + default: 12 + anObject: + type: 'object' + properties: + nestedInt: + type: 'integer' + default: 24 + nestedObject: + type: 'object' + properties: + superNestedInt: + type: 'integer' + default: 36 + + atom.config.setSchema('foo.bar', schema) + expect(atom.config.get("foo.bar.anInt")).toBe 12 + expect(atom.config.get("foo.bar.anObject")).toEqual + nestedInt: 24 + nestedObject: + superNestedInt: 36 + + it 'can set a non-object schema', -> + schema = + type: 'integer' + default: 12 + + atom.config.setSchema('foo.bar.anInt', schema) + expect(atom.config.get("foo.bar.anInt")).toBe 12 + expect(atom.config.getSchema('foo.bar.anInt')).toEqual + type: 'integer' + default: 12 + + it 'creates a properly nested schema', -> + schema = + type: 'object' + properties: + anInt: + type: 'integer' + default: 12 + + atom.config.setSchema('foo.bar', schema) + + expect(atom.config.schema).toEqual + type: 'object' + properties: + foo: + type: 'object' + properties: + bar: + type: 'object' + properties: + anInt: + type: 'integer' + default: 12 + + describe '.getSchema(keyPath)', -> + schema = + type: 'object' + properties: + anInt: + type: 'integer' + default: 12 + + atom.config.setSchema('foo.bar', schema) + + expect(atom.config.getSchema('foo.bar')).toEqual + type: 'object' + properties: + anInt: + type: 'integer' + default: 12 + + expect(atom.config.getSchema('foo.bar.anInt')).toEqual + type: 'integer' + default: 12 + diff --git a/src/config.coffee b/src/config.coffee index b0e6d64d3..911cffacb 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -31,6 +31,9 @@ class Config # Created during initialization, available as `atom.config` constructor: ({@configDirPath, @resourcePath}={}) -> @emitter = new Emitter + @schema = + type: 'object' + properties: {} @defaultSettings = {} @settings = {} @configFileHasErrors = false @@ -162,6 +165,13 @@ class Config isDefault: (keyPath) -> not _.valueForKeyPath(@settings, keyPath)? + getSchema: (keyPath) -> + keys = keyPath.split('.') + schema = @schema + for key in keys + schema = schema.properties[key] + schema + ### Section: To Deprecate ### @@ -300,6 +310,9 @@ class Config @watchSubscription = null setDefaults: (keyPath, defaults) -> + if typeof defaults isnt 'object' + return _.setValueForKeyPath(@defaultSettings, keyPath, defaults) + keys = keyPath.split('.') hash = @defaultSettings for key in keys @@ -310,6 +323,34 @@ class Config @emit 'updated' @emitter.emit 'did-change' + setSchema: (keyPath, schema) -> + unless typeof schema is "object" + throw new Error("Schemas can only be objects!") + + unless typeof schema.type? + throw new Error("Schema object's must have a type attribute") + + keys = keyPath.split('.') + rootSchema = @schema + for key in keys + rootSchema.type = 'object' + rootSchema.properties ?= {} + properties = rootSchema.properties + properties[key] ?= {} + rootSchema = properties[key] + + _.extend rootSchema, schema + @setDefaults(keyPath, @extractDefaultsFromSchema(schema)) + + extractDefaultsFromSchema: (schema) -> + if schema.default? + schema.default + else if schema.type is 'object' and schema.properties? and typeof schema.properties is "object" + defaults = {} + properties = schema.properties or {} + defaults[key] = @extractDefaultsFromSchema(value) for key, value of properties + defaults + update: -> return if @configFileHasErrors @save() From d0bb49dea08f5b4f0721076e40d1e9fdf565b02d Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 23 Sep 2014 15:32:42 -0700 Subject: [PATCH 015/145] Add type filter system to config --- spec/config-spec.coffee | 28 +++++++++++++++++++++++++++ src/config.coffee | 42 ++++++++++++++++++++++++++++++++++------- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 45c3f4104..bc9c4600d 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -486,3 +486,31 @@ describe "Config", -> type: 'integer' default: 12 + describe 'when the value has an integer type', -> + beforeEach -> + schema = + type: 'object' + properties: + anInt: + type: 'integer' + default: 12 + atom.config.setSchema('foo.bar', schema) + + it 'coerces a string to an int', -> + atom.config.set('foo.bar.anInt', '123') + expect(atom.config.get('foo.bar.anInt')).toBe 123 + + it 'coerces a float to an int', -> + atom.config.set('foo.bar.anInt', 12.3) + expect(atom.config.get('foo.bar.anInt')).toBe 12 + + describe 'when the value has a number type', -> + beforeEach -> + schema = + type: 'number' + default: 12.1 + atom.config.setSchema('foo.bar.anInt', schema) + + it 'coerces a string to a float', -> + atom.config.set('foo.bar.anInt', '12.23') + expect(atom.config.get('foo.bar.anInt')).toBe 12.23 diff --git a/src/config.coffee b/src/config.coffee index 911cffacb..b770883a8 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -27,6 +27,22 @@ pathWatcher = require 'pathwatcher' module.exports = class Config EmitterMixin.includeInto(this) + @typeFilters = {} + + @addTypeFilter: (typeName, filterFunction) -> + @typeFilters[typeName] ?= [] + @typeFilters[typeName].push(filterFunction) + + @addTypeFilters: (filters) -> + for typeName, functions of filters + for name, filterFunction of functions + @addTypeFilter(typeName, filterFunction) + + @executeTypeFilters: (value, schema) -> + if filterFunctions = @typeFilters[schema.type] + for filter in filterFunctions + value = filter.call(this, value, schema) + value # Created during initialization, available as `atom.config` constructor: ({@configDirPath, @resourcePath}={}) -> @@ -124,6 +140,7 @@ class Config # # Returns the `value`. set: (keyPath, value) -> + value = @scrubValue(keyPath, value) if @get(keyPath) isnt value defaultValue = _.valueForKeyPath(@defaultSettings, keyPath) value = undefined if _.isEqual(defaultValue, value) @@ -169,6 +186,7 @@ class Config keys = keyPath.split('.') schema = @schema for key in keys + break unless schema? schema = schema.properties[key] schema @@ -309,6 +327,15 @@ class Config @watchSubscription?.close() @watchSubscription = null + update: -> + return if @configFileHasErrors + @save() + @emit 'updated' + @emitter.emit 'did-change' + + save: -> + CSON.writeFileSync(@configFilePath, @settings) + setDefaults: (keyPath, defaults) -> if typeof defaults isnt 'object' return _.setValueForKeyPath(@defaultSettings, keyPath, defaults) @@ -351,11 +378,12 @@ class Config defaults[key] = @extractDefaultsFromSchema(value) for key, value of properties defaults - update: -> - return if @configFileHasErrors - @save() - @emit 'updated' - @emitter.emit 'did-change' + scrubValue: (keyPath, value) -> + value = @constructor.executeTypeFilters(value, schema) if schema = @getSchema(keyPath) + value - save: -> - CSON.writeFileSync(@configFilePath, @settings) +Config.addTypeFilters + 'integer': + coercion: (value, schema) -> parseInt(value) + 'number': + coercion: (value, schema) -> parseFloat(value) From f909d328262dcd0b5aa88e405686e94b8b250107 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 23 Sep 2014 16:40:35 -0700 Subject: [PATCH 016/145] Support more types --- spec/config-spec.coffee | 78 +++++++++++++++++++++++++++++++++++------ src/config.coffee | 37 ++++++++++++++++--- 2 files changed, 100 insertions(+), 15 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index bc9c4600d..b098098ed 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -486,15 +486,12 @@ describe "Config", -> type: 'integer' default: 12 - describe 'when the value has an integer type', -> + describe 'when the value has an "integer" type', -> beforeEach -> schema = - type: 'object' - properties: - anInt: - type: 'integer' - default: 12 - atom.config.setSchema('foo.bar', schema) + type: 'integer' + default: 12 + atom.config.setSchema('foo.bar.anInt', schema) it 'coerces a string to an int', -> atom.config.set('foo.bar.anInt', '123') @@ -504,13 +501,72 @@ describe "Config", -> atom.config.set('foo.bar.anInt', 12.3) expect(atom.config.get('foo.bar.anInt')).toBe 12 - describe 'when the value has a number type', -> + it 'will not set non-integers', -> + atom.config.set('foo.bar.anInt', null) + expect(atom.config.get('foo.bar.anInt')).toBe 12 + + describe 'when the value has an "integer" and "string" type', -> + beforeEach -> + schema = + type: ['integer', 'string'] + default: 12 + atom.config.setSchema('foo.bar.anInt', schema) + + it 'can coerce an int, and fallback to a string', -> + atom.config.set('foo.bar.anInt', '123') + expect(atom.config.get('foo.bar.anInt')).toBe 123 + + atom.config.set('foo.bar.anInt', 'cats') + expect(atom.config.get('foo.bar.anInt')).toBe 'cats' + + describe 'when the value has a "number" type', -> beforeEach -> schema = type: 'number' default: 12.1 - atom.config.setSchema('foo.bar.anInt', schema) + atom.config.setSchema('foo.bar.aFloat', schema) it 'coerces a string to a float', -> - atom.config.set('foo.bar.anInt', '12.23') - expect(atom.config.get('foo.bar.anInt')).toBe 12.23 + atom.config.set('foo.bar.aFloat', '12.23') + expect(atom.config.get('foo.bar.aFloat')).toBe 12.23 + + describe 'when the value has a "boolean" type', -> + beforeEach -> + schema = + type: 'boolean' + default: true + atom.config.setSchema('foo.bar.aBool', schema) + + it 'coerces various types to a boolean', -> + atom.config.set('foo.bar.aBool', 'true') + expect(atom.config.get('foo.bar.aBool')).toBe true + atom.config.set('foo.bar.aBool', 'false') + expect(atom.config.get('foo.bar.aBool')).toBe false + atom.config.set('foo.bar.aBool', 'TRUE') + expect(atom.config.get('foo.bar.aBool')).toBe true + atom.config.set('foo.bar.aBool', 'FALSE') + expect(atom.config.get('foo.bar.aBool')).toBe false + atom.config.set('foo.bar.aBool', 1) + expect(atom.config.get('foo.bar.aBool')).toBe true + atom.config.set('foo.bar.aBool', 0) + expect(atom.config.get('foo.bar.aBool')).toBe false + atom.config.set('foo.bar.aBool', {}) + expect(atom.config.get('foo.bar.aBool')).toBe true + atom.config.set('foo.bar.aBool', null) + expect(atom.config.get('foo.bar.aBool')).toBe false + + describe 'when the value has an "string" type', -> + beforeEach -> + schema = + type: 'string' + default: 'ok' + atom.config.setSchema('foo.bar.aString', schema) + + it 'allows strings', -> + atom.config.set('foo.bar.aString', 'yep') + expect(atom.config.get('foo.bar.aString')).toBe 'yep' + + it 'will not set non-strings', -> + atom.config.set('foo.bar.aString', null) + expect(atom.config.get('foo.bar.aString')).toBe 'ok' + diff --git a/src/config.coffee b/src/config.coffee index b770883a8..b49a2fc5d 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -39,9 +39,16 @@ class Config @addTypeFilter(typeName, filterFunction) @executeTypeFilters: (value, schema) -> - if filterFunctions = @typeFilters[schema.type] - for filter in filterFunctions - value = filter.call(this, value, schema) + types = schema.type + types = [types] unless Array.isArray(types) + for type in types + try + if filterFunctions = @typeFilters[type] + for filter in filterFunctions + value = filter.call(this, value, schema) + break + catch e + ; value # Created during initialization, available as `atom.config` @@ -384,6 +391,28 @@ class Config Config.addTypeFilters 'integer': - coercion: (value, schema) -> parseInt(value) + coercion: (value, schema) -> + value = parseInt(value) + throw new Error() if isNaN(value) + value + 'number': coercion: (value, schema) -> parseFloat(value) + + 'boolean': + coercion: (value, schema) -> + switch typeof value + when 'string' + value.toLowerCase() in ['true', 't'] + else + !!value + + 'string': (value, schema) -> + throw new Error unless typeof value is 'string' + value + + 'null': + # null sort of isnt supported. It will just unset in this case + coercion: (value, schema) -> + throw new Error() unless value == null + value From 1a8c5ba551c15c5586a6a4bc3cec50fe802221ed Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 23 Sep 2014 16:52:32 -0700 Subject: [PATCH 017/145] Handle validation of schema types --- spec/config-spec.coffee | 8 +++++++- src/config.coffee | 23 ++++++++++++++++------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index b098098ed..17b8cfcf8 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -505,6 +505,9 @@ describe "Config", -> atom.config.set('foo.bar.anInt', null) expect(atom.config.get('foo.bar.anInt')).toBe 12 + atom.config.set('foo.bar.anInt', 'nope') + expect(atom.config.get('foo.bar.anInt')).toBe 12 + describe 'when the value has an "integer" and "string" type', -> beforeEach -> schema = @@ -567,6 +570,9 @@ describe "Config", -> expect(atom.config.get('foo.bar.aString')).toBe 'yep' it 'will not set non-strings', -> - atom.config.set('foo.bar.aString', null) + expect(atom.config.set('foo.bar.aString', null)).toBe false + expect(atom.config.get('foo.bar.aString')).toBe 'ok' + + expect(atom.config.set('foo.bar.aString', 123)).toBe false expect(atom.config.get('foo.bar.aString')).toBe 'ok' diff --git a/src/config.coffee b/src/config.coffee index b49a2fc5d..bdd6f8335 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -39,6 +39,7 @@ class Config @addTypeFilter(typeName, filterFunction) @executeTypeFilters: (value, schema) -> + failedValidation = false types = schema.type types = [types] unless Array.isArray(types) for type in types @@ -46,9 +47,12 @@ class Config if filterFunctions = @typeFilters[type] for filter in filterFunctions value = filter.call(this, value, schema) + failedValidation = false break catch e - ; + failedValidation = true + + throw new Error('value is not valid') if failedValidation value # Created during initialization, available as `atom.config` @@ -145,15 +149,19 @@ class Config # * `keyPath` The {String} name of the key. # * `value` The value of the setting. # - # Returns the `value`. + # Returns a {Boolean} true if the value was set. set: (keyPath, value) -> - value = @scrubValue(keyPath, value) + try + value = @scrubValue(keyPath, value) + catch e + return false + if @get(keyPath) isnt value defaultValue = _.valueForKeyPath(@defaultSettings, keyPath) value = undefined if _.isEqual(defaultValue, value) _.setValueForKeyPath(@settings, keyPath, value) @update() - value + true # Extended: Get the {String} path to the config file being used. getUserConfigPath: -> @@ -407,9 +415,10 @@ Config.addTypeFilters else !!value - 'string': (value, schema) -> - throw new Error unless typeof value is 'string' - value + 'string': + typeCheck: (value, schema) -> + throw new Error() if typeof value isnt 'string' + value 'null': # null sort of isnt supported. It will just unset in this case From 2526ba0efb2e395b1370e19aead709f3452db73d Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 23 Sep 2014 17:03:51 -0700 Subject: [PATCH 018/145] Add an object filter --- spec/config-spec.coffee | 26 ++++++++++++++++++++++++++ src/config.coffee | 7 +++++++ 2 files changed, 33 insertions(+) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 17b8cfcf8..2d8a63794 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -576,3 +576,29 @@ describe "Config", -> expect(atom.config.set('foo.bar.aString', 123)).toBe false expect(atom.config.get('foo.bar.aString')).toBe 'ok' + describe 'when the value has an "object" type', -> + beforeEach -> + schema = + type: 'object' + properties: + anInt: + type: 'integer' + default: 12 + nestedObject: + type: 'object' + properties: + nestedBool: + type: 'boolean' + default: false + atom.config.setSchema('foo.bar', schema) + + it 'converts and validates all the children', -> + atom.config.set 'foo.bar', + anInt: '23' + nestedObject: + nestedBool: 't' + expect(atom.config.get('foo.bar')).toEqual + anInt: 23 + nestedObject: + nestedBool: true + diff --git a/src/config.coffee b/src/config.coffee index bdd6f8335..6394a7e77 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -425,3 +425,10 @@ Config.addTypeFilters coercion: (value, schema) -> throw new Error() unless value == null value + + 'object': + typeCheck: (value, schema) -> + return value unless schema.properties? + for prop, childSchema of schema.properties + value[prop] = @executeTypeFilters(value[prop], childSchema) if prop of value + value From 409b5536e198e3f1ac8737fe1bfb0d41975143b2 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 23 Sep 2014 18:27:24 -0700 Subject: [PATCH 019/145] Support arrays --- spec/config-spec.coffee | 12 ++++++++++++ src/config.coffee | 12 +++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 2d8a63794..509d32697 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -602,3 +602,15 @@ describe "Config", -> nestedObject: nestedBool: true + describe 'when the value has an "array" type', -> + beforeEach -> + schema = + type: 'array' + default: [1, 2, 3] + items: + type: 'integer' + atom.config.setSchema('foo.bar', schema) + + it 'converts an array of strings to an array of ints', -> + atom.config.set 'foo.bar', ['2', '3', '4'] + expect(atom.config.get('foo.bar')).toEqual [2, 3, 4] diff --git a/src/config.coffee b/src/config.coffee index 6394a7e77..a41dd5db7 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -427,8 +427,18 @@ Config.addTypeFilters value 'object': - typeCheck: (value, schema) -> + coercion: (value, schema) -> + throw new Error() if typeof value isnt 'object' return value unless schema.properties? for prop, childSchema of schema.properties value[prop] = @executeTypeFilters(value[prop], childSchema) if prop of value value + + 'array': + coercion: (value, schema) -> + throw new Error() unless Array.isArray(value) + itemSchema = schema.items + if itemSchema? + @executeTypeFilters(item, itemSchema) for item in value + else + value From ac67430926f5f902a8df1caf89802e36552794c3 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 23 Sep 2014 18:32:04 -0700 Subject: [PATCH 020/145] Handle bad values in number type --- spec/config-spec.coffee | 7 +++++++ src/config.coffee | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 509d32697..d2a2ec9b3 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -533,6 +533,13 @@ describe "Config", -> atom.config.set('foo.bar.aFloat', '12.23') expect(atom.config.get('foo.bar.aFloat')).toBe 12.23 + it 'will not set non-numbers', -> + atom.config.set('foo.bar.aFloat', null) + expect(atom.config.get('foo.bar.aFloat')).toBe 12.1 + + atom.config.set('foo.bar.aFloat', 'nope') + expect(atom.config.get('foo.bar.aFloat')).toBe 12.1 + describe 'when the value has a "boolean" type', -> beforeEach -> schema = diff --git a/src/config.coffee b/src/config.coffee index a41dd5db7..8f1c86080 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -405,7 +405,10 @@ Config.addTypeFilters value 'number': - coercion: (value, schema) -> parseFloat(value) + coercion: (value, schema) -> + value = parseFloat(value) + throw new Error() if isNaN(value) + value 'boolean': coercion: (value, schema) -> From f7f28e799589320cf729e19aa42d189d100fbf94 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 09:47:54 -0700 Subject: [PATCH 021/145] Handle `minimum` and `maximum` keywords on number types --- spec/config-spec.coffee | 32 ++++++++++++++++++++++++++++++++ src/config.coffee | 16 +++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index d2a2ec9b3..dd408bd44 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -508,6 +508,22 @@ describe "Config", -> atom.config.set('foo.bar.anInt', 'nope') expect(atom.config.get('foo.bar.anInt')).toBe 12 + describe 'when the minimum and maximum keys are used', -> + beforeEach -> + schema = + type: 'integer' + minimum: 10 + maximum: 20 + default: 12 + atom.config.setSchema('foo.bar.anInt', schema) + + it 'keeps the specified value within the specified range', -> + atom.config.set('foo.bar.anInt', '123') + expect(atom.config.get('foo.bar.anInt')).toBe 20 + + atom.config.set('foo.bar.anInt', '1') + expect(atom.config.get('foo.bar.anInt')).toBe 10 + describe 'when the value has an "integer" and "string" type', -> beforeEach -> schema = @@ -540,6 +556,22 @@ describe "Config", -> atom.config.set('foo.bar.aFloat', 'nope') expect(atom.config.get('foo.bar.aFloat')).toBe 12.1 + describe 'when the minimum and maximum keys are used', -> + beforeEach -> + schema = + type: 'number' + minimum: 11.2 + maximum: 25.4 + default: 12.1 + atom.config.setSchema('foo.bar.aFloat', schema) + + it 'keeps the specified value within the specified range', -> + atom.config.set('foo.bar.aFloat', '123.2') + expect(atom.config.get('foo.bar.aFloat')).toBe 25.4 + + atom.config.set('foo.bar.aFloat', '1.0') + expect(atom.config.get('foo.bar.aFloat')).toBe 11.2 + describe 'when the value has a "boolean" type', -> beforeEach -> schema = diff --git a/src/config.coffee b/src/config.coffee index 8f1c86080..482c40f4c 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -404,12 +404,26 @@ Config.addTypeFilters throw new Error() if isNaN(value) value + minMaxCoercion: (value, schema) -> + if schema.minimum? and typeof schema.minimum is 'number' + value = Math.max(value, schema.minimum) + if schema.maximum? and typeof schema.maximum is 'number' + value = Math.min(value, schema.maximum) + value + 'number': coercion: (value, schema) -> value = parseFloat(value) throw new Error() if isNaN(value) value + minMaxCoercion: (value, schema) -> + if schema.minimum? and typeof schema.minimum is 'number' + value = Math.max(value, schema.minimum) + if schema.maximum? and typeof schema.maximum is 'number' + value = Math.min(value, schema.maximum) + value + 'boolean': coercion: (value, schema) -> switch typeof value @@ -419,7 +433,7 @@ Config.addTypeFilters !!value 'string': - typeCheck: (value, schema) -> + coercion: (value, schema) -> throw new Error() if typeof value isnt 'string' value From 18e0adbfa884073a0a5ffafc0206ff7d5762c109 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 09:52:16 -0700 Subject: [PATCH 022/145] Fix linter error --- src/config.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index 482c40f4c..58a3296a1 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -110,9 +110,9 @@ class Config callback = options options = {} else - message = '' - message = '`callNow` as been set to false. Use ::onDidChange instead.' if options.callNow == false - deprecate 'Config::observe no longer supports options. #{message}' + message = "" + message = "`callNow` as been set to false. Use ::onDidChange instead." if options.callNow == false + deprecate "Config::observe no longer supports options. #{message}" callback(_.clone(@get(keyPath))) unless options.callNow == false @onDidChange(keyPath, callback) From 5e9a269278877c27d83c75d2cdaa0e12f59cf244 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 11:44:49 -0700 Subject: [PATCH 023/145] getSchema -> schemaForKeyPath --- spec/config-spec.coffee | 8 ++++---- src/config.coffee | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index dd408bd44..bd845132a 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -438,7 +438,7 @@ describe "Config", -> atom.config.setSchema('foo.bar.anInt', schema) expect(atom.config.get("foo.bar.anInt")).toBe 12 - expect(atom.config.getSchema('foo.bar.anInt')).toEqual + expect(atom.config.schemaForKeyPath('foo.bar.anInt')).toEqual type: 'integer' default: 12 @@ -465,7 +465,7 @@ describe "Config", -> type: 'integer' default: 12 - describe '.getSchema(keyPath)', -> + describe '.schemaForKeyPath(keyPath)', -> schema = type: 'object' properties: @@ -475,14 +475,14 @@ describe "Config", -> atom.config.setSchema('foo.bar', schema) - expect(atom.config.getSchema('foo.bar')).toEqual + expect(atom.config.schemaForKeyPath('foo.bar')).toEqual type: 'object' properties: anInt: type: 'integer' default: 12 - expect(atom.config.getSchema('foo.bar.anInt')).toEqual + expect(atom.config.schemaForKeyPath('foo.bar.anInt')).toEqual type: 'integer' default: 12 diff --git a/src/config.coffee b/src/config.coffee index 58a3296a1..7f0c8a862 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -197,7 +197,7 @@ class Config isDefault: (keyPath) -> not _.valueForKeyPath(@settings, keyPath)? - getSchema: (keyPath) -> + schemaForKeyPath: (keyPath) -> keys = keyPath.split('.') schema = @schema for key in keys @@ -394,7 +394,7 @@ class Config defaults scrubValue: (keyPath, value) -> - value = @constructor.executeTypeFilters(value, schema) if schema = @getSchema(keyPath) + value = @constructor.executeTypeFilters(value, schema) if schema = @schemaForKeyPath(keyPath) value Config.addTypeFilters From 9ff976021ea20ce3a5554c06f70287645909b622 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 12:12:11 -0700 Subject: [PATCH 024/145] Rename typeFilters to schemaValidators; add typeless validators --- src/config.coffee | 50 ++++++++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index 7f0c8a862..3fce27544 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -27,24 +27,25 @@ pathWatcher = require 'pathwatcher' module.exports = class Config EmitterMixin.includeInto(this) - @typeFilters = {} + @schemaValidators = {} - @addTypeFilter: (typeName, filterFunction) -> - @typeFilters[typeName] ?= [] - @typeFilters[typeName].push(filterFunction) + @addSchemaValidator: (typeName, validatorFunction) -> + @schemaValidators[typeName] ?= [] + @schemaValidators[typeName].push(validatorFunction) - @addTypeFilters: (filters) -> + @addSchemaValidators: (filters) -> for typeName, functions of filters - for name, filterFunction of functions - @addTypeFilter(typeName, filterFunction) + for name, validatorFunction of functions + @addSchemaValidator(typeName, validatorFunction) - @executeTypeFilters: (value, schema) -> + @executeSchemaValidators: (value, schema) -> failedValidation = false types = schema.type types = [types] unless Array.isArray(types) for type in types try - if filterFunctions = @typeFilters[type] + if filterFunctions = @schemaValidators[type] + filterFunctions = filterFunctions.concat(@schemaValidators['*']) for filter in filterFunctions value = filter.call(this, value, schema) failedValidation = false @@ -394,36 +395,22 @@ class Config defaults scrubValue: (keyPath, value) -> - value = @constructor.executeTypeFilters(value, schema) if schema = @schemaForKeyPath(keyPath) + value = @constructor.executeSchemaValidators(value, schema) if schema = @schemaForKeyPath(keyPath) value -Config.addTypeFilters +Config.addSchemaValidators 'integer': coercion: (value, schema) -> value = parseInt(value) throw new Error() if isNaN(value) value - minMaxCoercion: (value, schema) -> - if schema.minimum? and typeof schema.minimum is 'number' - value = Math.max(value, schema.minimum) - if schema.maximum? and typeof schema.maximum is 'number' - value = Math.min(value, schema.maximum) - value - 'number': coercion: (value, schema) -> value = parseFloat(value) throw new Error() if isNaN(value) value - minMaxCoercion: (value, schema) -> - if schema.minimum? and typeof schema.minimum is 'number' - value = Math.max(value, schema.minimum) - if schema.maximum? and typeof schema.maximum is 'number' - value = Math.min(value, schema.maximum) - value - 'boolean': coercion: (value, schema) -> switch typeof value @@ -448,7 +435,7 @@ Config.addTypeFilters throw new Error() if typeof value isnt 'object' return value unless schema.properties? for prop, childSchema of schema.properties - value[prop] = @executeTypeFilters(value[prop], childSchema) if prop of value + value[prop] = @executeSchemaValidators(value[prop], childSchema) if prop of value value 'array': @@ -456,6 +443,15 @@ Config.addTypeFilters throw new Error() unless Array.isArray(value) itemSchema = schema.items if itemSchema? - @executeTypeFilters(item, itemSchema) for item in value + @executeSchemaValidators(item, itemSchema) for item in value else value + + '*': + minimumAndMaximumCoercion: (value, schema) -> + return value unless typeof value is 'number' + if schema.minimum? and typeof schema.minimum is 'number' + value = Math.max(value, schema.minimum) + if schema.maximum? and typeof schema.maximum is 'number' + value = Math.min(value, schema.maximum) + value From 2c1190b55260fba247629556e91382e25ac01b0d Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 12:20:48 -0700 Subject: [PATCH 025/145] Validate enum keywords --- spec/config-spec.coffee | 43 +++++++++++++++++++++++++++++++++++++++++ src/config.coffee | 10 ++++++++++ 2 files changed, 53 insertions(+) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index bd845132a..1e2c14d8b 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -653,3 +653,46 @@ describe "Config", -> it 'converts an array of strings to an array of ints', -> atom.config.set 'foo.bar', ['2', '3', '4'] expect(atom.config.get('foo.bar')).toEqual [2, 3, 4] + + describe 'when the `enum` key is used', -> + beforeEach -> + schema = + type: 'object' + properties: + str: + type: 'string' + default: 'ok' + enum: ['ok', 'one', 'two'] + int: + type: 'integer' + default: 2 + enum: [2, 3, 5] + arr: + type: 'array' + default: ['one', 'two'] + items: + type: 'string' + enum: ['one', 'two', 'three'] + + atom.config.setSchema('foo.bar', schema) + + it 'will only set a string when the string is in the enum values', -> + expect(atom.config.set('foo.bar.str', 'nope')).toBe false + expect(atom.config.get('foo.bar.str')).toBe 'ok' + + expect(atom.config.set('foo.bar.str', 'one')).toBe true + expect(atom.config.get('foo.bar.str')).toBe 'one' + + it 'will only set an integer when the integer is in the enum values', -> + expect(atom.config.set('foo.bar.int', '400')).toBe false + expect(atom.config.get('foo.bar.int')).toBe 2 + + expect(atom.config.set('foo.bar.int', '3')).toBe true + expect(atom.config.get('foo.bar.int')).toBe 3 + + it 'will only set an array when the array values are in the enum values', -> + expect(atom.config.set('foo.bar.arr', ['one', 'two', 'five'])).toBe false + expect(atom.config.get('foo.bar.arr')).toEqual ['one', 'two'] + + expect(atom.config.set('foo.bar.arr', ['two', 'three'])).toBe true + expect(atom.config.get('foo.bar.arr')).toEqual ['two', 'three'] diff --git a/src/config.coffee b/src/config.coffee index 3fce27544..6c42480c0 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -455,3 +455,13 @@ Config.addSchemaValidators if schema.maximum? and typeof schema.maximum is 'number' value = Math.min(value, schema.maximum) value + + enumValidation: (value, schema) -> + possibleValues = schema.enum + return value unless possibleValues? and Array.isArray(possibleValues) and possibleValues.length + + for possibleValue in possibleValues + # Using `isEqual` for possibility of placing enums on array and object schemas + return value if _.isEqual(possibleValue, value) + + throw new Error('Value is not one of the possible values') From ba4df1b00270d86d66ae5f94c843f75a95732276 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 12:21:07 -0700 Subject: [PATCH 026/145] Pass a message to the errors thrown by validators --- src/config.coffee | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index 6c42480c0..688074992 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -402,13 +402,13 @@ Config.addSchemaValidators 'integer': coercion: (value, schema) -> value = parseInt(value) - throw new Error() if isNaN(value) + throw new Error('Value cannot be coerced into an int') if isNaN(value) value 'number': coercion: (value, schema) -> value = parseFloat(value) - throw new Error() if isNaN(value) + throw new Error('Value cannot be coerced into a number') if isNaN(value) value 'boolean': @@ -421,18 +421,18 @@ Config.addSchemaValidators 'string': coercion: (value, schema) -> - throw new Error() if typeof value isnt 'string' + throw new Error('Value must be a string') if typeof value isnt 'string' value 'null': # null sort of isnt supported. It will just unset in this case coercion: (value, schema) -> - throw new Error() unless value == null + throw new Error('Value must be an object') unless value == null value 'object': coercion: (value, schema) -> - throw new Error() if typeof value isnt 'object' + throw new Error('Value must be an object') if typeof value isnt 'object' return value unless schema.properties? for prop, childSchema of schema.properties value[prop] = @executeSchemaValidators(value[prop], childSchema) if prop of value @@ -440,7 +440,7 @@ Config.addSchemaValidators 'array': coercion: (value, schema) -> - throw new Error() unless Array.isArray(value) + throw new Error('Value must be an array') unless Array.isArray(value) itemSchema = schema.items if itemSchema? @executeSchemaValidators(item, itemSchema) for item in value From 9fbbd1e59bd5ce6f6ddbaf82eead65a846da9762 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 12:47:00 -0700 Subject: [PATCH 027/145] Back to getSchema --- spec/config-spec.coffee | 8 ++++---- src/config.coffee | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 1e2c14d8b..4d2db8667 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -438,7 +438,7 @@ describe "Config", -> atom.config.setSchema('foo.bar.anInt', schema) expect(atom.config.get("foo.bar.anInt")).toBe 12 - expect(atom.config.schemaForKeyPath('foo.bar.anInt')).toEqual + expect(atom.config.getSchema('foo.bar.anInt')).toEqual type: 'integer' default: 12 @@ -465,7 +465,7 @@ describe "Config", -> type: 'integer' default: 12 - describe '.schemaForKeyPath(keyPath)', -> + describe '.getSchema(keyPath)', -> schema = type: 'object' properties: @@ -475,14 +475,14 @@ describe "Config", -> atom.config.setSchema('foo.bar', schema) - expect(atom.config.schemaForKeyPath('foo.bar')).toEqual + expect(atom.config.getSchema('foo.bar')).toEqual type: 'object' properties: anInt: type: 'integer' default: 12 - expect(atom.config.schemaForKeyPath('foo.bar.anInt')).toEqual + expect(atom.config.getSchema('foo.bar.anInt')).toEqual type: 'integer' default: 12 diff --git a/src/config.coffee b/src/config.coffee index 688074992..8b26ad2f4 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -198,7 +198,7 @@ class Config isDefault: (keyPath) -> not _.valueForKeyPath(@settings, keyPath)? - schemaForKeyPath: (keyPath) -> + getSchema: (keyPath) -> keys = keyPath.split('.') schema = @schema for key in keys @@ -395,7 +395,7 @@ class Config defaults scrubValue: (keyPath, value) -> - value = @constructor.executeSchemaValidators(value, schema) if schema = @schemaForKeyPath(keyPath) + value = @constructor.executeSchemaValidators(value, schema) if schema = @getSchema(keyPath) value Config.addSchemaValidators From 601c603bbe654bac2444e4df6e26cb7f0463da47 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 12:47:06 -0700 Subject: [PATCH 028/145] :memo: --- src/config.coffee | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/config.coffee b/src/config.coffee index 8b26ad2f4..6e7fdbef4 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -398,6 +398,14 @@ class Config value = @constructor.executeSchemaValidators(value, schema) if schema = @getSchema(keyPath) value +# Base schema validators. These will coerce raw input into the specified type, +# and will throw an error when the value cannot be coerced. Throwing the error +# will indicate that the value should not be set. +# +# Validators are run from most specific to least. For a schema with type +# `integer`, all the validators for the `integer` type will be run first, in +# order of specification. Then the `*` validators will be run, in order of +# specification. Config.addSchemaValidators 'integer': coercion: (value, schema) -> From 74ba3c6a4958c3e161b4acc92cfbf1e9bea55078 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 13:12:37 -0700 Subject: [PATCH 029/145] Add config section to creating a package --- docs/creating-a-package.md | 117 +++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/docs/creating-a-package.md b/docs/creating-a-package.md index 288b5edd0..bdbfcf059 100644 --- a/docs/creating-a-package.md +++ b/docs/creating-a-package.md @@ -321,6 +321,122 @@ extensions your grammar supports: ] ``` +## Adding Configuration Settings + +You can support config options in your package that the user can edit in the +settings view. So do this you specify a `config` key in your package main +specifying the configuration. + +```coffeescript +module.exports = + config: # < Your config schema goes here! + #... + activate: (state) -> # ... + # ... +``` + +To specify the configuration, we use [json schema][json-schema] which allows you +to specify your value, the type it should be, etc. A simple example: + +```coffee +# We want to provide an `enableThing`, and a `thingVolume` +config: + enableThing: + type: 'boolean' + default: false + thingVolume: + type: 'integer' + default: 5 + minimum: 1 + maximum: 11 +``` + +The type keyword allows for type coercion and validation. If a `thingVolume` is +set to a string `'10'`, it will be coerced into an integer. + +```coffee +atom.config.set('my-package.thingVolume', '10') +atom.config.get('my-package.thingVolume') # -> 10 + +# It respects the min / max +atom.config.set('my-package.thingVolume', '400') +atom.config.get('my-package.thingVolume') # -> 11 + +# If it cannot be coerced, the value will not be set +atom.config.set('my-package.thingVolume', 'cats') +atom.config.get('my-package.thingVolume') # -> 11 +``` + +### Supported Types + +* `string` Values must be a string + ```coffee + config: + someSetting: + type: 'string' + default: 'hello' + ``` +* `integer` Values will be coerced into integer. Supports the `minimum` and `maximum` keys. + ```coffee + config: + someSetting: + type: 'integer' + default: 5 + minimum: 1 + maximum: 11 + ``` +* `number` Values will be coerced into a number, including real numbers. Supports the `minimum` and `maximum` keys. + ```coffee + config: + someSetting: + type: 'number' + default: 5.3 + minimum: 1.5 + maximum: 11.5 + ``` +* `boolean` Values will be coerced into a Boolean + ```coffee + config: + someSetting: + type: 'boolean' + default: false + ``` +* `array` Value must be an Array. The types of the values can be specified by a subschema in the `items` key. + ```coffee + config: + someSetting: + type: 'array' + default: [1, 2, 3] + items: + type: 'integer' + minimum: 1.5 + maximum: 11.5 + ``` +* `object` Value must be an object. This allows you to nest config options. Sub options must be under a `properties key` + ```coffee + config: + someSetting: + type: 'object' + properties: + myChildIntOption: + type: 'integer' + minimum: 1.5 + maximum: 11.5 + ``` + +### Other Supported Keys + +All schemas support an `enum` key. The enum key lets you specify all values that +the config setting can possibly be. + +```coffee +config: + someSetting: + type: 'integer' + default: 4 + enum: [2, 4, 6, 8] +``` + ## Bundle External Resources It's common to ship external resources like images and fonts in the package, to @@ -392,3 +508,4 @@ all the other available commands. [first-package]: your-first-package.html [convert-bundle]: converting-a-text-mate-bundle.html [convert-theme]: converting-a-text-mate-theme.html +[json-schema]: http://json-schema.org/ From 6a29630c8228a5f1ce9deccebb9ba2bf675aa192 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 13:18:04 -0700 Subject: [PATCH 030/145] Deprecate the getInt and getPositiveInt methods --- src/config.coffee | 74 ++++++++++++----------------------------------- 1 file changed, 18 insertions(+), 56 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index 6e7fdbef4..14c4c817b 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -206,57 +206,31 @@ class Config schema = schema.properties[key] schema - ### - Section: To Deprecate - ### - - # Retrieves the setting for the given key as an integer. - # - # * `keyPath` The {String} name of the key to retrieve - # - # Returns the value from Atom's default settings, the user's configuration - # file, or `NaN` if the key doesn't exist in either. - getInt: (keyPath) -> - parseInt(@get(keyPath)) - - # Retrieves the setting for the given key as a positive integer. - # - # * `keyPath` The {String} name of the key to retrieve - # * `defaultValue` The integer {Number} to fall back to if the value isn't - # positive, defaults to 0. - # - # Returns the value from Atom's default settings, the user's configuration - # file, or `defaultValue` if the key value isn't greater than zero. - getPositiveInt: (keyPath, defaultValue=0) -> - Math.max(@getInt(keyPath), 0) or defaultValue - - # Toggle the value at the key path. - # - # The new value will be `true` if the value is currently falsy and will be - # `false` if the value is currently truthy. - # - # * `keyPath` The {String} name of the key. - # - # Returns the new value. - toggle: (keyPath) -> - @set(keyPath, !@get(keyPath)) - ### Section: Deprecated ### - # Unobserve all callbacks on a given key. - # * `keyPath` The {String} name of the key to unobserve. - # + getInt: (keyPath) -> + deprecate '''Config::getInt is no longer necessary. Use ::get instead. + Make sure the config option you are accessing has specified an `integer` + schema. See the configuration section of + https://atom.io/docs/latest/creating-a-package for more info.''' + parseInt(@get(keyPath)) + + getPositiveInt: (keyPath, defaultValue=0) -> + deprecate '''Config::getPositiveInt is no longer necessary. Use ::get instead. + Make sure the config option you are accessing has specified an `integer` + schema with `minimum: 1`. See the configuration section of + https://atom.io/docs/latest/creating-a-package for more info.''' + Math.max(@getInt(keyPath), 0) or defaultValue + + toggle: (keyPath) -> + deprecate 'Config::toggle is no longer supported. Please remove from your code.' + @set(keyPath, !@get(keyPath)) + unobserve: (keyPath) -> deprecate 'Config::unobserve no longer does anything. Call `.dispose()` on the object returned by Config::observe instead.' - # Push the value to the array at the key path. - # - # * `keyPath` The {String} key path. - # * `value` The value to push to the array. - # - # Returns the new array length {Number} of the setting. pushAtKeyPath: (keyPath, value) -> deprecate 'Please remove from your code. Config::pushAtKeyPath is going away. Please push the value onto the array, and call Config::set' arrayValue = @get(keyPath) ? [] @@ -264,12 +238,6 @@ class Config @set(keyPath, arrayValue) result - # Add the value to the beginning of the array at the key path. - # - # * `keyPath` The {String} key path. - # * `value` The value to shift onto the array. - # - # Returns the new array length {Number} of the setting. unshiftAtKeyPath: (keyPath, value) -> deprecate 'Please remove from your code. Config::unshiftAtKeyPath is going away. Please unshift the value onto the array, and call Config::set' arrayValue = @get(keyPath) ? [] @@ -277,12 +245,6 @@ class Config @set(keyPath, arrayValue) result - # Remove the value from the array at the key path. - # - # * `keyPath` The {String} key path. - # * `value` The value to remove from the array. - # - # Returns the new array value of the setting. removeAtKeyPath: (keyPath, value) -> deprecate 'Please remove from your code. Config::removeAtKeyPath is going away. Please remove the value from the array, and call Config::set' arrayValue = @get(keyPath) ? [] From 9fff544955712fafd26efed2b551269572493d13 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 14:21:20 -0700 Subject: [PATCH 031/145] Fix specs --- spec/config-spec.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 4d2db8667..f884fbc13 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -12,7 +12,7 @@ describe "Config", -> describe ".get(keyPath)", -> it "allows a key path's value to be read", -> - expect(atom.config.set("foo.bar.baz", 42)).toBe 42 + expect(atom.config.set("foo.bar.baz", 42)).toBe true expect(atom.config.get("foo.bar.baz")).toBe 42 expect(atom.config.get("bogus.key.path")).toBeUndefined() @@ -37,7 +37,7 @@ describe "Config", -> describe ".set(keyPath, value)", -> it "allows a key path's value to be written", -> - expect(atom.config.set("foo.bar.baz", 42)).toBe 42 + expect(atom.config.set("foo.bar.baz", 42)).toBe true expect(atom.config.get("foo.bar.baz")).toBe 42 it "updates observers and saves when a key path is set", -> From 5bf09716ef754f47567c7c102670a6e6e33b6db2 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 14:37:09 -0700 Subject: [PATCH 032/145] convert the workspace config to use a schema --- spec/atom-spec.coffee | 7 +++++++ src/atom.coffee | 9 ++++++--- src/workspace-view.coffee | 30 ++++++++++++++++++++++-------- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/spec/atom-spec.coffee b/spec/atom-spec.coffee index 9fcfa1f2b..4add78e3d 100644 --- a/spec/atom-spec.coffee +++ b/spec/atom-spec.coffee @@ -47,3 +47,10 @@ describe "the `atom` global", -> [event, version, notes] = updateAvailableHandler.mostRecentCall.args expect(notes).toBe 'notes' expect(version).toBe 'version' + + describe "loading default config", -> + beforeEach -> + atom.loadConfig() + + it 'loads the default core config', -> + expect(atom.config.get('core.excludeVcsIgnoredPaths')).toBe true diff --git a/src/atom.coffee b/src/atom.coffee index ba209151f..35c5e9ae1 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -466,9 +466,7 @@ class Atom extends Model console.warn error.message if error? dimensions = @restoreWindowDimensions() - @config.load() - @config.setDefaults('core', require('./workspace-view').configDefaults) - @config.setDefaults('editor', require('./text-editor-view').configDefaults) + @loadConfig() @keymaps.loadBundledKeymaps() @themes.loadBaseStylesheets() @packages.loadPackages() @@ -604,6 +602,11 @@ class Atom extends Model @deserializeProject() @deserializeWorkspaceView() + loadConfig: -> + @config.load() + @config.setSchema('core', {type: 'object', properties: require('./workspace-view').config}) + @config.setDefaults('editor', require('./editor-view').configDefaults) + loadThemes: -> @themes.load() diff --git a/src/workspace-view.coffee b/src/workspace-view.coffee index 46de2ef49..03968a570 100644 --- a/src/workspace-view.coffee +++ b/src/workspace-view.coffee @@ -67,14 +67,28 @@ class WorkspaceView extends View @version: 4 - @configDefaults: - ignoredNames: [".git", ".hg", ".svn", ".DS_Store", "Thumbs.db"] - excludeVcsIgnoredPaths: true - disabledPackages: [] - themes: ['atom-dark-ui', 'atom-dark-syntax'] - projectHome: path.join(fs.getHomeDirectory(), 'github') - audioBeep: true - destroyEmptyPanes: true + @config: + ignoredNames: + type: 'array' + default: [".git", ".hg", ".svn", ".DS_Store", "Thumbs.db"] + excludeVcsIgnoredPaths: + type: 'boolean' + default: true + disabledPackages: + type: 'array' + default: [] + themes: + type: 'array' + default: ['atom-dark-ui', 'atom-dark-syntax'] + projectHome: + type: 'string' + default: path.join(fs.getHomeDirectory(), 'github') + audioBeep: + type: 'boolean' + default: true + destroyEmptyPanes: + type: 'boolean' + default: true @content: -> @div class: 'workspace', tabindex: -1, => From 5fdf3f894c3b45a963087f129d88f9ff138fe3b1 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 14:53:24 -0700 Subject: [PATCH 033/145] Load the config from Atom class so as not to duplicate --- spec/atom-spec.coffee | 3 --- spec/spec-helper.coffee | 6 +++--- src/atom.coffee | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/spec/atom-spec.coffee b/spec/atom-spec.coffee index 4add78e3d..b277a6efa 100644 --- a/spec/atom-spec.coffee +++ b/spec/atom-spec.coffee @@ -49,8 +49,5 @@ describe "the `atom` global", -> expect(version).toBe 'version' describe "loading default config", -> - beforeEach -> - atom.loadConfig() - it 'loads the default core config', -> expect(atom.config.get('core.excludeVcsIgnoredPaths')).toBe true diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index b94d33f0d..a427f21a6 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -91,16 +91,16 @@ beforeEach -> config = new Config({resourcePath, configDirPath: atom.getConfigDirPath()}) spyOn(config, 'load') spyOn(config, 'save') - config.setDefaults('core', WorkspaceView.configDefaults) - config.setDefaults('editor', TextEditorView.configDefaults) + atom.config = config + atom.loadConfig() config.set "core.destroyEmptyPanes", false config.set "editor.fontFamily", "Courier" config.set "editor.fontSize", 16 config.set "editor.autoIndent", false config.set "core.disabledPackages", ["package-that-throws-an-exception", "package-with-broken-package-json", "package-with-broken-keymap"] + config.load.reset() config.save.reset() - atom.config = config # make editor display updates synchronous spyOn(TextEditorView.prototype, 'requestDisplayUpdate').andCallFake -> @updateDisplay() diff --git a/src/atom.coffee b/src/atom.coffee index 35c5e9ae1..8cf823ba1 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -603,9 +603,9 @@ class Atom extends Model @deserializeWorkspaceView() loadConfig: -> - @config.load() @config.setSchema('core', {type: 'object', properties: require('./workspace-view').config}) @config.setDefaults('editor', require('./editor-view').configDefaults) + @config.load() loadThemes: -> @themes.load() From 0bb8821644aed5d905324e5ed8fd00d240c0fc55 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 15:00:45 -0700 Subject: [PATCH 034/145] Editor config uses a schema --- spec/atom-spec.coffee | 1 + src/atom.coffee | 4 +- src/text-editor-view.coffee | 80 +++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/spec/atom-spec.coffee b/spec/atom-spec.coffee index b277a6efa..31fc9b220 100644 --- a/spec/atom-spec.coffee +++ b/spec/atom-spec.coffee @@ -51,3 +51,4 @@ describe "the `atom` global", -> describe "loading default config", -> it 'loads the default core config', -> expect(atom.config.get('core.excludeVcsIgnoredPaths')).toBe true + expect(atom.config.get('editor.showInvisibles')).toBe false diff --git a/src/atom.coffee b/src/atom.coffee index 8cf823ba1..1160cb26f 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -603,8 +603,8 @@ class Atom extends Model @deserializeWorkspaceView() loadConfig: -> - @config.setSchema('core', {type: 'object', properties: require('./workspace-view').config}) - @config.setDefaults('editor', require('./editor-view').configDefaults) + @config.setSchema 'core', {type: 'object', properties: require('./workspace-view').config} + @config.setSchema 'editor', {type: 'object', properties: require('./editor-view').config} @config.load() loadThemes: -> diff --git a/src/text-editor-view.coffee b/src/text-editor-view.coffee index 48ed5b4c1..7f71699d4 100644 --- a/src/text-editor-view.coffee +++ b/src/text-editor-view.coffee @@ -36,6 +36,7 @@ TextEditorComponent = require './text-editor-component' # console.log(editorView.getModel().getPath()) # ``` module.exports = +<<<<<<< HEAD:src/text-editor-view.coffee class TextEditorView extends View @configDefaults: fontFamily: '' @@ -61,6 +62,85 @@ class TextEditorView extends View tab: '\u00bb' cr: '\u00a4' scrollPastEnd: false +======= +class EditorView extends View + @config: + fontFamily: + type: 'string' + default: '' + fontSize: + type: 'integer' + default: 16 + minimum: 1 + lineHeight: + type: 'number' + default: 1.3 + minimum: 1.0 + showInvisibles: + type: 'boolean' + default: false + showIndentGuide: + type: 'boolean' + default: false + showLineNumbers: + type: 'boolean' + default: true + autoIndent: + type: 'boolean' + default: true + normalizeIndentOnPaste: + type: 'boolean' + default: true + nonWordCharacters: + type: 'string' + default: "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-" + preferredLineLength: + type: 'integer' + default: 80 + minimum: 1 + tabLength: + type: 'integer' + default: 2 + minimum: 1 + softWrap: + type: 'boolean' + default: false + softTabs: + type: 'boolean' + default: true + softWrapAtPreferredLineLength: + type: 'boolean' + default: false + scrollSensitivity: + type: 'integer' + default: 40 + minimum: 1 + maximum: 200 + useHardwareAcceleration: + type: 'boolean' + default: true + confirmCheckoutHeadRevision: + type: 'boolean' + default: true + scrollPastEnd: + type: 'boolean' + default: false + invisibles: + type: 'object' + properties: + eol: + type: 'string' + default: '\u00ac' + space: + type: 'string' + default: '\u00b7' + tab: + type: 'string' + default: '\u00bb' + cr: + type: 'string' + default: '\u00a4' +>>>>>>> Editor config uses a schema:src/editor-view.coffee @content: (params) -> attributes = params.attributes ? {} From 9b0715833720c9abd1642d90d83de34a16ca2738 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 15:03:09 -0700 Subject: [PATCH 035/145] Add items schemas to arrays in workspaceView --- src/workspace-view.coffee | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/workspace-view.coffee b/src/workspace-view.coffee index 03968a570..6827a37d5 100644 --- a/src/workspace-view.coffee +++ b/src/workspace-view.coffee @@ -71,15 +71,21 @@ class WorkspaceView extends View ignoredNames: type: 'array' default: [".git", ".hg", ".svn", ".DS_Store", "Thumbs.db"] + items: + type: 'string' excludeVcsIgnoredPaths: type: 'boolean' default: true disabledPackages: type: 'array' default: [] + items: + type: 'string' themes: type: 'array' default: ['atom-dark-ui', 'atom-dark-syntax'] + items: + type: 'string' projectHome: type: 'string' default: path.join(fs.getHomeDirectory(), 'github') From fc3ba775c8c5648dd5c7b71076a323324980c372 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 15:20:35 -0700 Subject: [PATCH 036/145] Support schemas in packages --- .../package-with-config-schema/index.coffee | 13 +++++++++++++ spec/package-manager-spec.coffee | 16 +++++++++++++++- src/package.coffee | 5 ++++- 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 spec/fixtures/packages/package-with-config-schema/index.coffee diff --git a/spec/fixtures/packages/package-with-config-schema/index.coffee b/spec/fixtures/packages/package-with-config-schema/index.coffee new file mode 100644 index 000000000..7da1d67b7 --- /dev/null +++ b/spec/fixtures/packages/package-with-config-schema/index.coffee @@ -0,0 +1,13 @@ +module.exports = + config: + numbers: + type: 'object' + properties: + one: + type: 'integer' + default: 1 + two: + type: 'integer' + default: 2 + + activate: -> # no-op diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 16c697332..964dc5894 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -82,7 +82,21 @@ describe "PackageManager", -> expect(indexModule.activate).toHaveBeenCalled() expect(pack.mainModule).toBe indexModule - it "assigns config defaults from the module", -> + it "assigns config schema, including defaults when package contains a schema", -> + expect(atom.config.get('package-with-config-schema.numbers.one')).toBeUndefined() + + waitsForPromise -> + atom.packages.activatePackage('package-with-config-schema') + + runs -> + expect(atom.config.get('package-with-config-schema.numbers.one')).toBe 1 + expect(atom.config.get('package-with-config-schema.numbers.two')).toBe 2 + + expect(atom.config.set('package-with-config-schema.numbers.one', 'nope')).toBe false + expect(atom.config.set('package-with-config-schema.numbers.one', '10')).toBe true + expect(atom.config.get('package-with-config-schema.numbers.one')).toBe 10 + + it "still assigns configDefaults from the module though deprecated", -> expect(atom.config.get('package-with-config-defaults.numbers.one')).toBeUndefined() waitsForPromise -> diff --git a/src/package.coffee b/src/package.coffee index 30cd1ad2f..65442bf6f 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -143,7 +143,10 @@ class Package @requireMainModule() if @mainModule? - atom.config.setDefaults(@name, @mainModule.configDefaults) + if @mainModule.config? and typeof @mainModule.config is 'object' + atom.config.setSchema @name, {type: 'object', properties: @mainModule.config} + else if @mainModule.configDefaults? and typeof @mainModule.configDefaults is 'object' + atom.config.setDefaults(@name, @mainModule.configDefaults) @mainModule.activateConfig?() @configActivated = true From f57dbfd9f531c847f8e1ce34beab2b526f71554c Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 15:38:13 -0700 Subject: [PATCH 037/145] Deprecate configDefaults in packages. --- src/package.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/package.coffee b/src/package.coffee index 65442bf6f..dcb64056d 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -146,6 +146,9 @@ class Package if @mainModule.config? and typeof @mainModule.config is 'object' atom.config.setSchema @name, {type: 'object', properties: @mainModule.config} else if @mainModule.configDefaults? and typeof @mainModule.configDefaults is 'object' + deprecate """Use a config schema instead. See the configuration section + of https://atom.io/docs/latest/creating-a-package and + https://atom.io/docs/api/latest/Config for more details""" atom.config.setDefaults(@name, @mainModule.configDefaults) @mainModule.activateConfig?() @configActivated = true From 0d2fdec326341264e250103f07b5a190bab1d26c Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 15:46:49 -0700 Subject: [PATCH 038/145] Fix specs in config --- spec/config-spec.coffee | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index f884fbc13..42beb2c44 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -164,13 +164,19 @@ describe "Config", -> expect(observeHandler).toHaveBeenCalledWith atom.config.get("foo.bar.baz"), {previous: ['a', 'b', 'c']} describe ".getPositiveInt(keyPath, defaultValue)", -> - it "returns the proper current or default value", -> + it "returns the proper coerced value", -> atom.config.set('editor.preferredLineLength', 0) - expect(atom.config.getPositiveInt('editor.preferredLineLength', 80)).toBe 80 + expect(atom.config.getPositiveInt('editor.preferredLineLength', 80)).toBe 1 + + it "returns the proper coerced value", -> atom.config.set('editor.preferredLineLength', -1234) - expect(atom.config.getPositiveInt('editor.preferredLineLength', 80)).toBe 80 + expect(atom.config.getPositiveInt('editor.preferredLineLength', 80)).toBe 1 + + it "returns the default value when a string is passed in", -> atom.config.set('editor.preferredLineLength', 'abcd') expect(atom.config.getPositiveInt('editor.preferredLineLength', 80)).toBe 80 + + it "returns the default value when null is passed in", -> atom.config.set('editor.preferredLineLength', null) expect(atom.config.getPositiveInt('editor.preferredLineLength', 80)).toBe 80 @@ -452,18 +458,15 @@ describe "Config", -> atom.config.setSchema('foo.bar', schema) - expect(atom.config.schema).toEqual + expect(atom.config.getSchema('foo')).toEqual type: 'object' properties: - foo: + bar: type: 'object' properties: - bar: - type: 'object' - properties: - anInt: - type: 'integer' - default: 12 + anInt: + type: 'integer' + default: 12 describe '.getSchema(keyPath)', -> schema = From 969ca048e889da47bc936747f6463901c9b193bc Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 15:54:02 -0700 Subject: [PATCH 039/145] Fix specs --- spec/tokenized-buffer-spec.coffee | 2 +- src/text-editor-view.coffee | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index ec1dc5009..51d48b5eb 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -586,7 +586,7 @@ describe "TokenizedBuffer", -> atom.config.set('editor.tabLength', 1) expect(tokenizedBuffer.tokenForPosition([0,0]).value).toBe ' ' atom.config.set('editor.tabLength', 0) - expect(tokenizedBuffer.tokenForPosition([0,0]).value).toBe ' ' + expect(tokenizedBuffer.tokenForPosition([0,0]).value).toBe ' ' describe "when the invisibles value changes", -> beforeEach -> diff --git a/src/text-editor-view.coffee b/src/text-editor-view.coffee index 7f71699d4..3746f43f2 100644 --- a/src/text-editor-view.coffee +++ b/src/text-editor-view.coffee @@ -73,9 +73,8 @@ class EditorView extends View default: 16 minimum: 1 lineHeight: - type: 'number' + type: 'string' default: 1.3 - minimum: 1.0 showInvisibles: type: 'boolean' default: false From af1bdaf901a6ef3f3cc7e6bcd4d06471a88b28e2 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 16:14:47 -0700 Subject: [PATCH 040/145] Dont fail when there are thigns to set with array and object types --- spec/config-spec.coffee | 9 +++++++++ src/config.coffee | 17 ++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 42beb2c44..8a5c04a00 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -644,6 +644,15 @@ describe "Config", -> nestedObject: nestedBool: true + it 'will set only the values that adhere to the schema', -> + expect(atom.config.set 'foo.bar', + anInt: 'nope' + nestedObject: + nestedBool: true + ).toBe true + expect(atom.config.get('foo.bar.anInt')).toEqual 12 + expect(atom.config.get('foo.bar.nestedObject.nestedBool')).toEqual true + describe 'when the value has an "array" type', -> beforeEach -> schema = diff --git a/src/config.coffee b/src/config.coffee index 14c4c817b..b82fce05e 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -404,16 +404,27 @@ Config.addSchemaValidators coercion: (value, schema) -> throw new Error('Value must be an object') if typeof value isnt 'object' return value unless schema.properties? + newValue = {} for prop, childSchema of schema.properties - value[prop] = @executeSchemaValidators(value[prop], childSchema) if prop of value - value + continue unless prop of value + try + newValue[prop] = @executeSchemaValidators(value[prop], childSchema) + catch error + ; + newValue 'array': coercion: (value, schema) -> throw new Error('Value must be an array') unless Array.isArray(value) itemSchema = schema.items if itemSchema? - @executeSchemaValidators(item, itemSchema) for item in value + newValue = [] + for item in value + try + newValue.push @executeSchemaValidators(item, itemSchema) + catch error + ; + newValue else value From 832b4ae4d8b35d8487f4429873097efc4f3352a9 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 16:24:26 -0700 Subject: [PATCH 041/145] Fix specs --- spec/text-editor-component-spec.coffee | 4 ++-- src/text-editor-view.coffee | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index cc76c4f6f..180efeeb5 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -1734,11 +1734,11 @@ describe "TextEditorComponent", -> nextAnimationFrame() expect(verticalScrollbarNode.scrollTop).toBe 10 - it "parses negative scrollSensitivity values as positive", -> + it "parses negative scrollSensitivity values at the minimum", -> atom.config.set('editor.scrollSensitivity', -50) componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -10)) nextAnimationFrame() - expect(verticalScrollbarNode.scrollTop).toBe 5 + expect(verticalScrollbarNode.scrollTop).toBe 1 describe "when the mousewheel event's target is a line", -> it "keeps the line on the DOM if it is scrolled off-screen", -> diff --git a/src/text-editor-view.coffee b/src/text-editor-view.coffee index 3746f43f2..a457100d3 100644 --- a/src/text-editor-view.coffee +++ b/src/text-editor-view.coffee @@ -113,7 +113,7 @@ class EditorView extends View scrollSensitivity: type: 'integer' default: 40 - minimum: 1 + minimum: 10 maximum: 200 useHardwareAcceleration: type: 'boolean' @@ -128,16 +128,16 @@ class EditorView extends View type: 'object' properties: eol: - type: 'string' + type: ['string', 'boolean'] default: '\u00ac' space: - type: 'string' + type: ['string', 'boolean'] default: '\u00b7' tab: - type: 'string' + type: ['string', 'boolean'] default: '\u00bb' cr: - type: 'string' + type: ['string', 'boolean'] default: '\u00a4' >>>>>>> Editor config uses a schema:src/editor-view.coffee From aa5b0ce41f3d81c73e753fbd8cf3abdcf98b9eac Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 16:34:39 -0700 Subject: [PATCH 042/145] Remove linter errors, warn when bad value --- src/config.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index b82fce05e..f67a63232 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -410,7 +410,7 @@ Config.addSchemaValidators try newValue[prop] = @executeSchemaValidators(value[prop], childSchema) catch error - ; + console.warn "Error setting value #{error.message}" newValue 'array': @@ -423,7 +423,7 @@ Config.addSchemaValidators try newValue.push @executeSchemaValidators(item, itemSchema) catch error - ; + console.warn "Error setting value #{error.message}" newValue else value From 0fc773c1fc2c02b8da103dca732752750c6827fd Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 17:13:01 -0700 Subject: [PATCH 043/145] Warn when loading bogus values from the user's config --- spec/config-spec.coffee | 32 ++++++++++++++++++++++++++++++++ src/config.coffee | 21 ++++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 8a5c04a00..f17fc47f7 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -360,6 +360,38 @@ describe "Config", -> expect(fs.existsSync(atom.config.configFilePath)).toBe true expect(CSON.readFileSync(atom.config.configFilePath)).toEqual {} + describe "when a schema is specified", -> + beforeEach -> + schema = + type: 'object' + properties: + bar: + type: 'string' + default: 'def' + int: + type: 'integer' + default: 12 + + atom.config.setSchema('foo', schema) + + describe "when the config file contains values that do not adhere to the schema", -> + warnSpy = null + beforeEach -> + warnSpy = spyOn console, 'warn' + fs.writeFileSync atom.config.configFilePath, """ + foo: + bar: 'baz' + int: 'bad value' + """ + atom.config.loadUserConfig() + + it "updates the config data based on the file contents", -> + expect(atom.config.get("foo.bar")).toBe 'baz' + expect(atom.config.get("foo.int")).toBe 12 + + expect(warnSpy).toHaveBeenCalled() + expect(warnSpy.mostRecentCall.args[0]).toContain "'foo.int' could not be set" + describe ".observeUserConfig()", -> updatedHandler = null diff --git a/src/config.coffee b/src/config.coffee index f67a63232..50ae711a4 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -284,7 +284,7 @@ class Config try userConfig = CSON.readFileSync(@configFilePath) - _.extend(@settings, userConfig) + @setAllRecursive(userConfig) @configFileHasErrors = false @emit 'updated' @emitter.emit 'did-change' @@ -314,6 +314,22 @@ class Config save: -> CSON.writeFileSync(@configFilePath, @settings) + setAllRecursive: (value) -> + @setRecursive(key, childValue) for key, childValue of value + return + + setRecursive: (keyPath, value) -> + if value? and isPlainObject(value) + keys = keyPath.split('.') + for key, childValue of value + continue unless value.hasOwnProperty(key) + @setRecursive(keys.concat([key]).join('.'), childValue) + else + unless @set(keyPath, value) + console.warn("'#{keyPath}' could not be set. Attempted value: #{JSON.stringify(value)}; Schema: #{JSON.stringify(@getSchema(keyPath))}") + + return + setDefaults: (keyPath, defaults) -> if typeof defaults isnt 'object' return _.setValueForKeyPath(@defaultSettings, keyPath, defaults) @@ -446,3 +462,6 @@ Config.addSchemaValidators return value if _.isEqual(possibleValue, value) throw new Error('Value is not one of the possible values') + +isPlainObject = (value) -> + _.isObject(value) and not _.isArray(value) and not _.isFunction(value) and not _.isString(value) From 662fc443dc9f3b146f034a4b364b0fb4747b5e6d Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 17:50:36 -0700 Subject: [PATCH 044/145] Fix specs --- spec/config-spec.coffee | 4 ++-- src/config.coffee | 17 +++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index f17fc47f7..3264a9fd9 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -735,8 +735,8 @@ describe "Config", -> expect(atom.config.get('foo.bar.int')).toBe 3 it 'will only set an array when the array values are in the enum values', -> - expect(atom.config.set('foo.bar.arr', ['one', 'two', 'five'])).toBe false - expect(atom.config.get('foo.bar.arr')).toEqual ['one', 'two'] + expect(atom.config.set('foo.bar.arr', ['one', 'five'])).toBe true + expect(atom.config.get('foo.bar.arr')).toEqual ['one'] expect(atom.config.set('foo.bar.arr', ['two', 'three'])).toBe true expect(atom.config.get('foo.bar.arr')).toEqual ['two', 'three'] diff --git a/src/config.coffee b/src/config.coffee index 50ae711a4..97c3f99bc 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -39,7 +39,7 @@ class Config @addSchemaValidator(typeName, validatorFunction) @executeSchemaValidators: (value, schema) -> - failedValidation = false + error = null types = schema.type types = [types] unless Array.isArray(types) for type in types @@ -48,12 +48,12 @@ class Config filterFunctions = filterFunctions.concat(@schemaValidators['*']) for filter in filterFunctions value = filter.call(this, value, schema) - failedValidation = false + error = null break catch e - failedValidation = true + error = e - throw new Error('value is not valid') if failedValidation + throw error if error? value # Created during initialization, available as `atom.config` @@ -325,7 +325,12 @@ class Config continue unless value.hasOwnProperty(key) @setRecursive(keys.concat([key]).join('.'), childValue) else - unless @set(keyPath, value) + try + value = @scrubValue(keyPath, value) + defaultValue = _.valueForKeyPath(@defaultSettings, keyPath) + value = undefined if _.isEqual(defaultValue, value) + _.setValueForKeyPath(@settings, keyPath, value) + catch e console.warn("'#{keyPath}' could not be set. Attempted value: #{JSON.stringify(value)}; Schema: #{JSON.stringify(@getSchema(keyPath))}") return @@ -439,7 +444,7 @@ Config.addSchemaValidators try newValue.push @executeSchemaValidators(item, itemSchema) catch error - console.warn "Error setting value #{error.message}" + console.warn "Error setting value: #{error.message}" newValue else value From 694dd05e7b431eaa2ea2d2c0f3a28f38f47419d3 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 18:04:36 -0700 Subject: [PATCH 045/145] Make warn messages way better. --- src/config.coffee | 49 ++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index 97c3f99bc..502cb9ed5 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -38,7 +38,7 @@ class Config for name, validatorFunction of functions @addSchemaValidator(typeName, validatorFunction) - @executeSchemaValidators: (value, schema) -> + @executeSchemaValidators: (keyPath, value, schema) -> error = null types = schema.type types = [types] unless Array.isArray(types) @@ -47,7 +47,7 @@ class Config if filterFunctions = @schemaValidators[type] filterFunctions = filterFunctions.concat(@schemaValidators['*']) for filter in filterFunctions - value = filter.call(this, value, schema) + value = filter.call(this, keyPath, value, schema) error = null break catch e @@ -378,7 +378,7 @@ class Config defaults scrubValue: (keyPath, value) -> - value = @constructor.executeSchemaValidators(value, schema) if schema = @getSchema(keyPath) + value = @constructor.executeSchemaValidators(keyPath, value, schema) if schema = @getSchema(keyPath) value # Base schema validators. These will coerce raw input into the specified type, @@ -391,19 +391,19 @@ class Config # specification. Config.addSchemaValidators 'integer': - coercion: (value, schema) -> + coercion: (keyPath, value, schema) -> value = parseInt(value) - throw new Error('Value cannot be coerced into an int') if isNaN(value) + throw new Error("Cannot set #{keyPath}, #{JSON.stringify(value)} cannot be coerced into an int") if isNaN(value) value 'number': - coercion: (value, schema) -> + coercion: (keyPath, value, schema) -> value = parseFloat(value) - throw new Error('Value cannot be coerced into a number') if isNaN(value) + throw new Error("Cannot set #{keyPath}, #{JSON.stringify(value)} cannot be coerced into a number") if isNaN(value) value 'boolean': - coercion: (value, schema) -> + coercion: (keyPath, value, schema) -> switch typeof value when 'string' value.toLowerCase() in ['true', 't'] @@ -411,46 +411,47 @@ Config.addSchemaValidators !!value 'string': - coercion: (value, schema) -> - throw new Error('Value must be a string') if typeof value isnt 'string' + coercion: (keyPath, value, schema) -> + throw new Error("Cannot set #{keyPath}, #{JSON.stringify(value)} must be a string") if typeof value isnt 'string' value 'null': # null sort of isnt supported. It will just unset in this case - coercion: (value, schema) -> - throw new Error('Value must be an object') unless value == null + coercion: (keyPath, value, schema) -> + throw new Error("Cannot set #{keyPath}, #{JSON.stringify(value)} must be null") unless value == null value 'object': - coercion: (value, schema) -> - throw new Error('Value must be an object') if typeof value isnt 'object' + coercion: (keyPath, value, schema) -> + throw new Error("Cannot set #{keyPath}, #{JSON.stringify(value)} must be an object") unless isPlainObject(value) return value unless schema.properties? + newValue = {} for prop, childSchema of schema.properties - continue unless prop of value + continue unless value.hasOwnProperty(prop) try - newValue[prop] = @executeSchemaValidators(value[prop], childSchema) + newValue[prop] = @executeSchemaValidators("#{keyPath}.#{prop}", value[prop], childSchema) catch error - console.warn "Error setting value #{error.message}" + console.warn "Error setting item in object: #{error.message}" newValue 'array': - coercion: (value, schema) -> - throw new Error('Value must be an array') unless Array.isArray(value) + coercion: (keyPath, value, schema) -> + throw new Error("Cannot set #{keyPath}, #{JSON.stringify(value)} must be an array") unless Array.isArray(value) itemSchema = schema.items if itemSchema? newValue = [] for item in value try - newValue.push @executeSchemaValidators(item, itemSchema) + newValue.push @executeSchemaValidators(keyPath, item, itemSchema) catch error - console.warn "Error setting value: #{error.message}" + console.warn "Error setting item in array: #{error.message}" newValue else value '*': - minimumAndMaximumCoercion: (value, schema) -> + minimumAndMaximumCoercion: (keyPath, value, schema) -> return value unless typeof value is 'number' if schema.minimum? and typeof schema.minimum is 'number' value = Math.max(value, schema.minimum) @@ -458,7 +459,7 @@ Config.addSchemaValidators value = Math.min(value, schema.maximum) value - enumValidation: (value, schema) -> + enumValidation: (keyPath, value, schema) -> possibleValues = schema.enum return value unless possibleValues? and Array.isArray(possibleValues) and possibleValues.length @@ -466,7 +467,7 @@ Config.addSchemaValidators # Using `isEqual` for possibility of placing enums on array and object schemas return value if _.isEqual(possibleValue, value) - throw new Error('Value is not one of the possible values') + throw new Error("Cannot set #{keyPath}, #{JSON.stringify(value)} is not one of #{JSON.stringify(possibleValues)}") isPlainObject = (value) -> _.isObject(value) and not _.isArray(value) and not _.isFunction(value) and not _.isString(value) From ae76bd6c96e766d2ecc6d328c0d52363dd1eff59 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 18:08:11 -0700 Subject: [PATCH 046/145] Do not allow infinity in number types --- spec/config-spec.coffee | 4 ++++ src/config.coffee | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 3264a9fd9..6845b0c89 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -532,6 +532,10 @@ describe "Config", -> atom.config.set('foo.bar.anInt', '123') expect(atom.config.get('foo.bar.anInt')).toBe 123 + it 'does not allow infinity', -> + atom.config.set('foo.bar.anInt', Infinity) + expect(atom.config.get('foo.bar.anInt')).toBe 12 + it 'coerces a float to an int', -> atom.config.set('foo.bar.anInt', 12.3) expect(atom.config.get('foo.bar.anInt')).toBe 12 diff --git a/src/config.coffee b/src/config.coffee index 502cb9ed5..698d86ddc 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -393,13 +393,13 @@ Config.addSchemaValidators 'integer': coercion: (keyPath, value, schema) -> value = parseInt(value) - throw new Error("Cannot set #{keyPath}, #{JSON.stringify(value)} cannot be coerced into an int") if isNaN(value) + throw new Error("Cannot set #{keyPath}, #{JSON.stringify(value)} cannot be coerced into an int") if isNaN(value) or not isFinite(value) value 'number': coercion: (keyPath, value, schema) -> value = parseFloat(value) - throw new Error("Cannot set #{keyPath}, #{JSON.stringify(value)} cannot be coerced into a number") if isNaN(value) + throw new Error("Cannot set #{keyPath}, #{JSON.stringify(value)} cannot be coerced into a number") if isNaN(value) or not isFinite(value) value 'boolean': From beb96cc02569fdac2b8b79d90812c250195345fc Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 24 Sep 2014 19:39:47 -0700 Subject: [PATCH 047/145] :lipstick: --- src/config.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.coffee b/src/config.coffee index 698d86ddc..88106071b 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -112,7 +112,7 @@ class Config options = {} else message = "" - message = "`callNow` as been set to false. Use ::onDidChange instead." if options.callNow == false + message = "`callNow` was set to false. Use ::onDidChange instead." if options.callNow == false deprecate "Config::observe no longer supports options. #{message}" callback(_.clone(@get(keyPath))) unless options.callNow == false From 03a9a67ba843179e5e750ce23b0d04befa7501c1 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 10:17:54 -0700 Subject: [PATCH 048/145] Move spec --- spec/config-spec.coffee | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 6845b0c89..4e7aad526 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -442,6 +442,26 @@ describe "Config", -> schema = null describe '.setSchema(keyPath, schema)', -> + it 'creates a properly nested schema', -> + schema = + type: 'object' + properties: + anInt: + type: 'integer' + default: 12 + + atom.config.setSchema('foo.bar', schema) + + expect(atom.config.getSchema('foo')).toEqual + type: 'object' + properties: + bar: + type: 'object' + properties: + anInt: + type: 'integer' + default: 12 + it 'sets defaults specified by the schema', -> schema = type: 'object' @@ -480,26 +500,6 @@ describe "Config", -> type: 'integer' default: 12 - it 'creates a properly nested schema', -> - schema = - type: 'object' - properties: - anInt: - type: 'integer' - default: 12 - - atom.config.setSchema('foo.bar', schema) - - expect(atom.config.getSchema('foo')).toEqual - type: 'object' - properties: - bar: - type: 'object' - properties: - anInt: - type: 'integer' - default: 12 - describe '.getSchema(keyPath)', -> schema = type: 'object' From 98e828b33751f12de112b1febf26c2ef0f06e7e8 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 10:18:21 -0700 Subject: [PATCH 049/145] Move default schema into config-default-schema.coffee --- src/atom.coffee | 3 +- src/config-default-schema.coffee | 113 +++++++++++++++++++++++++++++++ src/config.coffee | 28 ++++---- src/text-editor-view.coffee | 104 ---------------------------- src/workspace-view.coffee | 30 -------- 5 files changed, 128 insertions(+), 150 deletions(-) create mode 100644 src/config-default-schema.coffee diff --git a/src/atom.coffee b/src/atom.coffee index 1160cb26f..9c507928e 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -603,8 +603,7 @@ class Atom extends Model @deserializeWorkspaceView() loadConfig: -> - @config.setSchema 'core', {type: 'object', properties: require('./workspace-view').config} - @config.setSchema 'editor', {type: 'object', properties: require('./editor-view').config} + @config.setSchema null, {type: 'object', properties: _.clone(require('./config-default-schema'))} @config.load() loadThemes: -> diff --git a/src/config-default-schema.coffee b/src/config-default-schema.coffee new file mode 100644 index 000000000..4dc96d1cd --- /dev/null +++ b/src/config-default-schema.coffee @@ -0,0 +1,113 @@ +path = require 'path' +fs = require 'fs-plus' + +# This is loaded by atom.coffee +module.exports = + core: + type: 'object' + properties: + ignoredNames: + type: 'array' + default: [".git", ".hg", ".svn", ".DS_Store", "Thumbs.db"] + items: + type: 'string' + excludeVcsIgnoredPaths: + type: 'boolean' + default: true + disabledPackages: + type: 'array' + default: [] + items: + type: 'string' + themes: + type: 'array' + default: ['atom-dark-ui', 'atom-dark-syntax'] + items: + type: 'string' + projectHome: + type: 'string' + default: path.join(fs.getHomeDirectory(), 'github') + audioBeep: + type: 'boolean' + default: true + destroyEmptyPanes: + type: 'boolean' + default: true + + editor: + type: 'object' + properties: + fontFamily: + type: 'string' + default: '' + fontSize: + type: 'integer' + default: 16 + minimum: 1 + lineHeight: + type: 'string' + default: 1.3 + showInvisibles: + type: 'boolean' + default: false + showIndentGuide: + type: 'boolean' + default: false + showLineNumbers: + type: 'boolean' + default: true + autoIndent: + type: 'boolean' + default: true + normalizeIndentOnPaste: + type: 'boolean' + default: true + nonWordCharacters: + type: 'string' + default: "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-" + preferredLineLength: + type: 'integer' + default: 80 + minimum: 1 + tabLength: + type: 'integer' + default: 2 + minimum: 1 + softWrap: + type: 'boolean' + default: false + softTabs: + type: 'boolean' + default: true + softWrapAtPreferredLineLength: + type: 'boolean' + default: false + scrollSensitivity: + type: 'integer' + default: 40 + minimum: 10 + maximum: 200 + scrollPastEnd: + type: 'boolean' + default: false + useHardwareAcceleration: + type: 'boolean' + default: true + confirmCheckoutHeadRevision: + type: 'boolean' + default: true + invisibles: + type: 'object' + properties: + eol: + type: ['string', 'boolean'] + default: '\u00ac' + space: + type: ['string', 'boolean'] + default: '\u00b7' + tab: + type: ['string', 'boolean'] + default: '\u00bb' + cr: + type: ['string', 'boolean'] + default: '\u00a4' diff --git a/src/config.coffee b/src/config.coffee index 88106071b..b2164f8c7 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -336,14 +336,14 @@ class Config return setDefaults: (keyPath, defaults) -> - if typeof defaults isnt 'object' + unless isPlainObject(defaults) return _.setValueForKeyPath(@defaultSettings, keyPath, defaults) - keys = keyPath.split('.') hash = @defaultSettings - for key in keys - hash[key] ?= {} - hash = hash[key] + if keyPath + for key in keyPath.split('.') + hash[key] ?= {} + hash = hash[key] _.extend hash, defaults @emit 'updated' @@ -351,19 +351,19 @@ class Config setSchema: (keyPath, schema) -> unless typeof schema is "object" - throw new Error("Schemas can only be objects!") + throw new Error("Error loading schema for #{keyPath}: schemas can only be objects!") unless typeof schema.type? - throw new Error("Schema object's must have a type attribute") + throw new Error("Error loading schema for #{keyPath}: schema objects must have a type attribute") - keys = keyPath.split('.') rootSchema = @schema - for key in keys - rootSchema.type = 'object' - rootSchema.properties ?= {} - properties = rootSchema.properties - properties[key] ?= {} - rootSchema = properties[key] + if keyPath + for key in keyPath.split('.') + rootSchema.type = 'object' + rootSchema.properties ?= {} + properties = rootSchema.properties + properties[key] ?= {} + rootSchema = properties[key] _.extend rootSchema, schema @setDefaults(keyPath, @extractDefaultsFromSchema(schema)) diff --git a/src/text-editor-view.coffee b/src/text-editor-view.coffee index a457100d3..b84a8adcf 100644 --- a/src/text-editor-view.coffee +++ b/src/text-editor-view.coffee @@ -36,111 +36,7 @@ TextEditorComponent = require './text-editor-component' # console.log(editorView.getModel().getPath()) # ``` module.exports = -<<<<<<< HEAD:src/text-editor-view.coffee class TextEditorView extends View - @configDefaults: - fontFamily: '' - fontSize: 16 - lineHeight: 1.3 - showInvisibles: false - showIndentGuide: false - showLineNumbers: true - autoIndent: true - normalizeIndentOnPaste: true - nonWordCharacters: "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-" - preferredLineLength: 80 - tabLength: 2 - softWrap: false - softTabs: true - softWrapAtPreferredLineLength: false - scrollSensitivity: 40 - useHardwareAcceleration: true - confirmCheckoutHeadRevision: true - invisibles: - eol: '\u00ac' - space: '\u00b7' - tab: '\u00bb' - cr: '\u00a4' - scrollPastEnd: false -======= -class EditorView extends View - @config: - fontFamily: - type: 'string' - default: '' - fontSize: - type: 'integer' - default: 16 - minimum: 1 - lineHeight: - type: 'string' - default: 1.3 - showInvisibles: - type: 'boolean' - default: false - showIndentGuide: - type: 'boolean' - default: false - showLineNumbers: - type: 'boolean' - default: true - autoIndent: - type: 'boolean' - default: true - normalizeIndentOnPaste: - type: 'boolean' - default: true - nonWordCharacters: - type: 'string' - default: "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-" - preferredLineLength: - type: 'integer' - default: 80 - minimum: 1 - tabLength: - type: 'integer' - default: 2 - minimum: 1 - softWrap: - type: 'boolean' - default: false - softTabs: - type: 'boolean' - default: true - softWrapAtPreferredLineLength: - type: 'boolean' - default: false - scrollSensitivity: - type: 'integer' - default: 40 - minimum: 10 - maximum: 200 - useHardwareAcceleration: - type: 'boolean' - default: true - confirmCheckoutHeadRevision: - type: 'boolean' - default: true - scrollPastEnd: - type: 'boolean' - default: false - invisibles: - type: 'object' - properties: - eol: - type: ['string', 'boolean'] - default: '\u00ac' - space: - type: ['string', 'boolean'] - default: '\u00b7' - tab: - type: ['string', 'boolean'] - default: '\u00bb' - cr: - type: ['string', 'boolean'] - default: '\u00a4' ->>>>>>> Editor config uses a schema:src/editor-view.coffee - @content: (params) -> attributes = params.attributes ? {} attributes.class = 'editor react editor-colors' diff --git a/src/workspace-view.coffee b/src/workspace-view.coffee index 6827a37d5..f011d4818 100644 --- a/src/workspace-view.coffee +++ b/src/workspace-view.coffee @@ -6,7 +6,6 @@ Delegator = require 'delegato' {deprecate, logDeprecationWarnings} = require 'grim' scrollbarStyle = require 'scrollbar-style' {$, $$, View} = require './space-pen-extensions' -fs = require 'fs-plus' Workspace = require './workspace' CommandInstaller = require './command-installer' PaneView = require './pane-view' @@ -67,35 +66,6 @@ class WorkspaceView extends View @version: 4 - @config: - ignoredNames: - type: 'array' - default: [".git", ".hg", ".svn", ".DS_Store", "Thumbs.db"] - items: - type: 'string' - excludeVcsIgnoredPaths: - type: 'boolean' - default: true - disabledPackages: - type: 'array' - default: [] - items: - type: 'string' - themes: - type: 'array' - default: ['atom-dark-ui', 'atom-dark-syntax'] - items: - type: 'string' - projectHome: - type: 'string' - default: path.join(fs.getHomeDirectory(), 'github') - audioBeep: - type: 'boolean' - default: true - destroyEmptyPanes: - type: 'boolean' - default: true - @content: -> @div class: 'workspace', tabindex: -1, => @div class: 'horizontal', outlet: 'horizontal', => From f09e58b434974824d6928706682798ddec9156d5 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 11:11:52 -0700 Subject: [PATCH 050/145] Update config docs --- docs/creating-a-package.md | 110 ++----------------- src/config.coffee | 213 +++++++++++++++++++++++++++++++++++-- 2 files changed, 213 insertions(+), 110 deletions(-) diff --git a/docs/creating-a-package.md b/docs/creating-a-package.md index bdbfcf059..c2f74e164 100644 --- a/docs/creating-a-package.md +++ b/docs/creating-a-package.md @@ -329,113 +329,21 @@ specifying the configuration. ```coffeescript module.exports = - config: # < Your config schema goes here! - #... + # Your config schema! + config: + someInt: + type: 'integer' + default: 23 + minimum: 1 activate: (state) -> # ... # ... ``` To specify the configuration, we use [json schema][json-schema] which allows you -to specify your value, the type it should be, etc. A simple example: +to specify your value, the type it should be, etc. -```coffee -# We want to provide an `enableThing`, and a `thingVolume` -config: - enableThing: - type: 'boolean' - default: false - thingVolume: - type: 'integer' - default: 5 - minimum: 1 - maximum: 11 -``` - -The type keyword allows for type coercion and validation. If a `thingVolume` is -set to a string `'10'`, it will be coerced into an integer. - -```coffee -atom.config.set('my-package.thingVolume', '10') -atom.config.get('my-package.thingVolume') # -> 10 - -# It respects the min / max -atom.config.set('my-package.thingVolume', '400') -atom.config.get('my-package.thingVolume') # -> 11 - -# If it cannot be coerced, the value will not be set -atom.config.set('my-package.thingVolume', 'cats') -atom.config.get('my-package.thingVolume') # -> 11 -``` - -### Supported Types - -* `string` Values must be a string - ```coffee - config: - someSetting: - type: 'string' - default: 'hello' - ``` -* `integer` Values will be coerced into integer. Supports the `minimum` and `maximum` keys. - ```coffee - config: - someSetting: - type: 'integer' - default: 5 - minimum: 1 - maximum: 11 - ``` -* `number` Values will be coerced into a number, including real numbers. Supports the `minimum` and `maximum` keys. - ```coffee - config: - someSetting: - type: 'number' - default: 5.3 - minimum: 1.5 - maximum: 11.5 - ``` -* `boolean` Values will be coerced into a Boolean - ```coffee - config: - someSetting: - type: 'boolean' - default: false - ``` -* `array` Value must be an Array. The types of the values can be specified by a subschema in the `items` key. - ```coffee - config: - someSetting: - type: 'array' - default: [1, 2, 3] - items: - type: 'integer' - minimum: 1.5 - maximum: 11.5 - ``` -* `object` Value must be an object. This allows you to nest config options. Sub options must be under a `properties key` - ```coffee - config: - someSetting: - type: 'object' - properties: - myChildIntOption: - type: 'integer' - minimum: 1.5 - maximum: 11.5 - ``` - -### Other Supported Keys - -All schemas support an `enum` key. The enum key lets you specify all values that -the config setting can possibly be. - -```coffee -config: - someSetting: - type: 'integer' - default: 4 - enum: [2, 4, 6, 8] -``` +See the [Config API Docs](https://atom.io/docs/api/latest/Config) for more +details specifying your configuration. ## Bundle External Resources diff --git a/src/config.coffee b/src/config.coffee index b2164f8c7..4ba183ff5 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -12,18 +12,213 @@ pathWatcher = require 'pathwatcher' # # An instance of this class is always available as the `atom.config` global. # -# ## Best practices -# -# * Create your own root keypath using your package's name. -# * Don't depend on (or write to) configuration keys outside of your keypath. -# -# ## Examples +# ## Getting and setting config settings # # ```coffee -# atom.config.set('my-package.key', 'value') -# atom.config.observe 'my-package.key', -> -# console.log 'My configuration changed:', atom.config.get('my-package.key') +# # When no value has been set, `::get` returns the setting's default value +# atom.config.get('my-package.myKey') # -> 'defaultValue' +# +# atom.config.set('my-package.myKey', 'value') +# atom.config.get('my-package.myKey') # -> 'value' # ``` +# +# You may want to watch for changes. Use {::observe} to catch changes to the setting. +# +# ```coffee +# atom.config.set('my-package.myKey', 'value') +# atom.config.observe 'my-package.myKey', (newValue) -> +# # `observe` calls your callback immediately and every time the value is changed +# console.log 'My configuration changed:', newValue +# ``` +# +# If you'd like to get a notification only when the value changes, use {::onDidChange}. +# +# ```coffee +# atom.config.onDidChange 'my-package.myKey', (newValue) -> +# console.log 'My configuration changed:', newValue +# ``` +# +# ### Value Coercion +# +# Config settings each have a type specified by way of a +# [schema](json-schema.org). Let's say we have an integer setting that only +# allows numbers greater than 0: +# +# ```coffee +# # When no value has been set, `::get` returns the setting's default value +# atom.config.get('my-package.anInt') # -> 12 +# +# # The string will be coerced to the integer 123 +# atom.config.set('my-package.anInt', '123') +# atom.config.get('my-package.anInt') # -> 123 +# +# # The string will be coerced to an integer, but it must be greater than 0, so is set to 1 +# atom.config.set('my-package.anInt', '-20') +# atom.config.get('my-package.anInt') # -> 1 +# ``` +# +# ## Defining config settings for your package +# +# You specify a schema under a `config` key +# +# ```coffeescript +# module.exports = +# # Your config schema +# config: +# someInt: +# type: 'integer' +# default: 23 +# minimum: 1 +# +# activate: (state) -> # ... +# # ... +# ``` +# +# ## Config Schemas +# +# We use [json schema][json-schema] which allows you to specify your value, its +# default, the type it should be, etc. A simple example: +# +# ```coffee +# # We want to provide an `enableThing`, and a `thingVolume` +# config: +# enableThing: +# type: 'boolean' +# default: false +# thingVolume: +# type: 'integer' +# default: 5 +# minimum: 1 +# maximum: 11 +# ``` +# +# The type keyword allows for type coercion and validation. If a `thingVolume` is +# set to a string `'10'`, it will be coerced into an integer. +# +# ```coffee +# atom.config.set('my-package.thingVolume', '10') +# atom.config.get('my-package.thingVolume') # -> 10 +# +# # It respects the min / max +# atom.config.set('my-package.thingVolume', '400') +# atom.config.get('my-package.thingVolume') # -> 11 +# +# # If it cannot be coerced, the value will not be set +# atom.config.set('my-package.thingVolume', 'cats') +# atom.config.get('my-package.thingVolume') # -> 11 +# ``` +# +# ### Supported Types +# +# * __string__ Values must be a string +# ```coffee +# config: +# someSetting: +# type: 'string' +# default: 'hello' +# ``` +# * __integer__ Values will be coerced into integer. Supports the (optional) `minimum` and `maximum` keys. +# ```coffee +# config: +# someSetting: +# type: 'integer' +# default: 5 +# minimum: 1 +# maximum: 11 +# ``` +# * __number__ Values will be coerced into a number, including real numbers. Supports the (optional) `minimum` and `maximum` keys. +# ```coffee +# config: +# someSetting: +# type: 'number' +# default: 5.3 +# minimum: 1.5 +# maximum: 11.5 +# ``` +# * __boolean__ Values will be coerced into a Boolean +# ```coffee +# config: +# someSetting: +# type: 'boolean' +# default: false +# ``` +# * __array__ Value must be an Array. The types of the values can be specified by a subschema in the `items` key. +# ```coffee +# config: +# someSetting: +# type: 'array' +# default: [1, 2, 3] +# items: +# type: 'integer' +# minimum: 1.5 +# maximum: 11.5 +# ``` +# * __object__ Value must be an object. This allows you to nest config options. Sub options must be under a `properties key` +# ```coffee +# config: +# someSetting: +# type: 'object' +# properties: +# myChildIntOption: +# type: 'integer' +# minimum: 1.5 +# maximum: 11.5 +# ``` +# +# ### Other Supported Keys +# +# #### enum +# +# All schemas support an `enum` key. The enum key lets you specify all values +# that the config setting can possibly be. `enum` _must_ be an array of values +# of your specified type. +# +# ```coffee +# config: +# someSetting: +# type: 'integer' +# default: 4 +# enum: [2, 4, 6, 8] +# ``` +# +# ```coffee +# atom.config.set('my-package.someSetting', '2') +# atom.config.get('my-package.someSetting') # -> 2 +# +# # will not set values outside of the enum values +# atom.config.set('my-package.someSetting', '3') +# atom.config.get('my-package.someSetting') # -> 2 +# +# # If it cannot be coerced, the value will not be set +# atom.config.set('my-package.someSetting', '4') +# atom.config.get('my-package.someSetting') # -> 4 +# ``` +# +# #### title and description +# +# The settings view will use the `title` and `description` keys display your +# option in a readable way. By default the settings view humanizes your config +# key, so `someSetting` becomes `Some Setting`. In some cases, this is confusing +# for users, and a more descriptive title is useful. +# +# Descriptions will be displayed below the title in the settings view. +# +# ```coffee +# config: +# someSetting: +# title: 'Setting Magnitude' +# description: 'This will affect the blah and the other blah' +# type: 'integer' +# default: 4 +# ``` +# +# __Note__: You should strive to be so clear in your naming of the config +# setting that you do not need to specify a title or description! +# +# ## Best practices +# +# * Don't depend on (or write to) configuration keys outside of your keypath. +# module.exports = class Config EmitterMixin.includeInto(this) From 8f738aae5345ae14af3ab0e8d8276a699fd49eca Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 11:59:01 -0700 Subject: [PATCH 051/145] Fix up Config doc string --- src/config.coffee | 162 +++++++++++++++++++++++++++------------------- 1 file changed, 97 insertions(+), 65 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index 4ba183ff5..fa83fa9bb 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -15,11 +15,11 @@ pathWatcher = require 'pathwatcher' # ## Getting and setting config settings # # ```coffee -# # When no value has been set, `::get` returns the setting's default value -# atom.config.get('my-package.myKey') # -> 'defaultValue' +# # With no value set, `::get` returns the setting's default value +# atom.config.get('my-package.myKey') # -> 'defaultValue' # -# atom.config.set('my-package.myKey', 'value') -# atom.config.get('my-package.myKey') # -> 'value' +# atom.config.set('my-package.myKey', 'value') +# atom.config.get('my-package.myKey') # -> 'value' # ``` # # You may want to watch for changes. Use {::observe} to catch changes to the setting. @@ -27,11 +27,11 @@ pathWatcher = require 'pathwatcher' # ```coffee # atom.config.set('my-package.myKey', 'value') # atom.config.observe 'my-package.myKey', (newValue) -> -# # `observe` calls your callback immediately and every time the value is changed +# # `observe` calls immediately and every time the value is changed # console.log 'My configuration changed:', newValue # ``` # -# If you'd like to get a notification only when the value changes, use {::onDidChange}. +# If you want a notification only when the value changes, use {::onDidChange}. # # ```coffee # atom.config.onDidChange 'my-package.myKey', (newValue) -> @@ -41,27 +41,27 @@ pathWatcher = require 'pathwatcher' # ### Value Coercion # # Config settings each have a type specified by way of a -# [schema](json-schema.org). Let's say we have an integer setting that only -# allows numbers greater than 0: +# [schema](json-schema.org). For example we might an integer setting that only +# allows integers greater than `0`: # # ```coffee -# # When no value has been set, `::get` returns the setting's default value -# atom.config.get('my-package.anInt') # -> 12 +# # When no value has been set, `::get` returns the setting's default value +# atom.config.get('my-package.anInt') # -> 12 # -# # The string will be coerced to the integer 123 -# atom.config.set('my-package.anInt', '123') -# atom.config.get('my-package.anInt') # -> 123 +# # The string will be coerced to the integer 123 +# atom.config.set('my-package.anInt', '123') +# atom.config.get('my-package.anInt') # -> 123 # -# # The string will be coerced to an integer, but it must be greater than 0, so is set to 1 -# atom.config.set('my-package.anInt', '-20') -# atom.config.get('my-package.anInt') # -> 1 +# # The string will be coerced to an integer, but it must be greater than 0, so is set to 1 +# atom.config.set('my-package.anInt', '-20') +# atom.config.get('my-package.anInt') # -> 1 # ``` # -# ## Defining config settings for your package +# ## Defining settings for your package # -# You specify a schema under a `config` key +# Specify a schema under a `config` key in your package main. # -# ```coffeescript +# ```coffee # module.exports = # # Your config schema # config: @@ -74,9 +74,12 @@ pathWatcher = require 'pathwatcher' # # ... # ``` # +# See [Creating a Package](https://atom.io/docs/latest/creating-a-package) for +# more info. +# # ## Config Schemas # -# We use [json schema][json-schema] which allows you to specify your value, its +# We use [json schema](json-schema.org) which allows you to specify your value, its # default, the type it should be, etc. A simple example: # # ```coffee @@ -110,14 +113,22 @@ pathWatcher = require 'pathwatcher' # # ### Supported Types # -# * __string__ Values must be a string -# ```coffee -# config: -# someSetting: -# type: 'string' -# default: 'hello' -# ``` -# * __integer__ Values will be coerced into integer. Supports the (optional) `minimum` and `maximum` keys. +# #### string +# +# Values must be a string. +# +# ```coffee +# config: +# someSetting: +# type: 'string' +# default: 'hello' +# ``` +# +# #### integer +# +# Values will be coerced into integer. Supports the (optional) `minimum` and +# `maximum` keys. +# # ```coffee # config: # someSetting: @@ -126,50 +137,71 @@ pathWatcher = require 'pathwatcher' # minimum: 1 # maximum: 11 # ``` -# * __number__ Values will be coerced into a number, including real numbers. Supports the (optional) `minimum` and `maximum` keys. -# ```coffee -# config: -# someSetting: -# type: 'number' -# default: 5.3 +# +# #### number +# +# Values will be coerced into a number, including real numbers. Supports the +# (optional) `minimum` and `maximum` keys. +# +# ```coffee +# config: +# someSetting: +# type: 'number' +# default: 5.3 +# minimum: 1.5 +# maximum: 11.5 +# ``` +# +# #### boolean +# +# Values will be coerced into a Boolean. `'true'` and `'t'` will be coerced into +# `true`. Numbers, arrays, objects, and anything else will be coerced via +# `!!value`. +# +# ```coffee +# config: +# someSetting: +# type: 'boolean' +# default: false +# ``` +# +# #### array +# +# Value must be an Array. The types of the values can be specified by a +# subschema in the `items` key. +# +# ```coffee +# config: +# someSetting: +# type: 'array' +# default: [1, 2, 3] +# items: +# type: 'integer' # minimum: 1.5 # maximum: 11.5 -# ``` -# * __boolean__ Values will be coerced into a Boolean -# ```coffee -# config: -# someSetting: -# type: 'boolean' -# default: false -# ``` -# * __array__ Value must be an Array. The types of the values can be specified by a subschema in the `items` key. -# ```coffee -# config: -# someSetting: -# type: 'array' -# default: [1, 2, 3] -# items: +# ``` +# +# #### object +# +# Value must be an object. This allows you to nest config options. Sub options +# must be under a `properties key` +# +# ```coffee +# config: +# someSetting: +# type: 'object' +# properties: +# myChildIntOption: # type: 'integer' # minimum: 1.5 # maximum: 11.5 -# ``` -# * __object__ Value must be an object. This allows you to nest config options. Sub options must be under a `properties key` -# ```coffee -# config: -# someSetting: -# type: 'object' -# properties: -# myChildIntOption: -# type: 'integer' -# minimum: 1.5 -# maximum: 11.5 -# ``` +# ``` # # ### Other Supported Keys # # #### enum # -# All schemas support an `enum` key. The enum key lets you specify all values +# All types support an `enum` key. The enum key lets you specify all values # that the config setting can possibly be. `enum` _must_ be an array of values # of your specified type. # @@ -197,9 +229,9 @@ pathWatcher = require 'pathwatcher' # #### title and description # # The settings view will use the `title` and `description` keys display your -# option in a readable way. By default the settings view humanizes your config -# key, so `someSetting` becomes `Some Setting`. In some cases, this is confusing -# for users, and a more descriptive title is useful. +# config setting in a readable way. By default the settings view humanizes your +# config key, so `someSetting` becomes `Some Setting`. In some cases, this is +# confusing for users, and a more descriptive title is useful. # # Descriptions will be displayed below the title in the settings view. # From 885a19492ce36d354e86df8c83b6a8b87ae7c4e2 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 11:59:44 -0700 Subject: [PATCH 052/145] Rearrange managing settings section --- src/config.coffee | 68 ++++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index fa83fa9bb..20dbd013c 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -299,28 +299,6 @@ class Config Section: Config Subscription ### - # Essential: Add a listener for changes to a given key path. - # - # * `keyPath` The {String} name of the key to observe - # * `callback` The {Function} to call when the value of the key changes. - # The first argument will be the new value of the key and the - #   second argument will be an {Object} with a `previous` property - # that is the prior value of the key. - # - # Returns a {Disposable} with the following keys on which you can call - # `.dispose()` to unsubscribe. - onDidChange: (keyPath, callback) -> - value = @get(keyPath) - previousValue = _.clone(value) - updateCallback = => - value = @get(keyPath) - unless _.isEqual(value, previousValue) - previous = previousValue - previousValue = _.clone(value) - callback(value, {previous}) - - @emitter.on 'did-change', updateCallback - # Essential: Add a listener for changes to a given key path. This is different # than {::onDidChange} in that it will immediately call your callback with the # current value of the config entry. @@ -345,8 +323,30 @@ class Config callback(_.clone(@get(keyPath))) unless options.callNow == false @onDidChange(keyPath, callback) + # Essential: Add a listener for changes to a given key path. + # + # * `keyPath` The {String} name of the key to observe + # * `callback` The {Function} to call when the value of the key changes. + # The first argument will be the new value of the key and the + #   second argument will be an {Object} with a `previous` property + # that is the prior value of the key. + # + # Returns a {Disposable} with the following keys on which you can call + # `.dispose()` to unsubscribe. + onDidChange: (keyPath, callback) -> + value = @get(keyPath) + previousValue = _.clone(value) + updateCallback = => + value = @get(keyPath) + unless _.isEqual(value, previousValue) + previous = previousValue + previousValue = _.clone(value) + callback(value, {previous}) + + @emitter.on 'did-change', updateCallback + ### - Section: get / set + Section: Managing Settings ### # Essential: Retrieves the setting for the given key. @@ -354,7 +354,7 @@ class Config # * `keyPath` The {String} name of the key to retrieve. # # Returns the value from Atom's default settings, the user's configuration - # file, or `null` if the key doesn't exist in either. + # file in the type specified by the configuration schema. get: (keyPath) -> value = _.valueForKeyPath(@settings, keyPath) defaultValue = _.valueForKeyPath(@defaultSettings, keyPath) @@ -377,7 +377,9 @@ class Config # * `keyPath` The {String} name of the key. # * `value` The value of the setting. # - # Returns a {Boolean} true if the value was set. + # Returns a {Boolean} + # * `true` if the value was set. + # * `false` if the value was not able to be coerced to the type specified in the setting's schema. set: (keyPath, value) -> try value = @scrubValue(keyPath, value) @@ -391,14 +393,6 @@ class Config @update() true - # Extended: Get the {String} path to the config file being used. - getUserConfigPath: -> - @configFilePath - - # Extended: Returns a new {Object} containing all of settings and defaults. - getSettings: -> - _.deepExtend(@settings, @defaultSettings) - # Extended: Restore the key path to its default value. # # * `keyPath` The {String} name of the key. @@ -433,6 +427,14 @@ class Config schema = schema.properties[key] schema + # Extended: Returns a new {Object} containing all of settings and defaults. + getSettings: -> + _.deepExtend(@settings, @defaultSettings) + + # Extended: Get the {String} path to the config file being used. + getUserConfigPath: -> + @configFilePath + ### Section: Deprecated ### From cb1f8e02aa09007d1c1716468d9a115e0c61dcee Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 12:00:17 -0700 Subject: [PATCH 053/145] Return the value from `restoreDefault` --- src/config.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config.coffee b/src/config.coffee index 20dbd013c..cf3902284 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -400,6 +400,7 @@ class Config # Returns the new value. restoreDefault: (keyPath) -> @set(keyPath, _.valueForKeyPath(@defaultSettings, keyPath)) + @get(keyPath) # Extended: Get the default value of the key path. # From c6f7c75c8a37da880470a190f23b5a059050a73b Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 12:00:32 -0700 Subject: [PATCH 054/145] Update method doc strings for clarity --- src/config.coffee | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/config.coffee b/src/config.coffee index cf3902284..d5fe28dd5 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -402,7 +402,9 @@ class Config @set(keyPath, _.valueForKeyPath(@defaultSettings, keyPath)) @get(keyPath) - # Extended: Get the default value of the key path. + # Extended: Get the default value of the key path. _Please note_ that in most + # cases calling this is not necessary! {::get} returns the default value when + # a custom value is not specified. # # * `keyPath` The {String} name of the key. # @@ -420,6 +422,14 @@ class Config isDefault: (keyPath) -> not _.valueForKeyPath(@settings, keyPath)? + # Extended: Retrieve the schema for a specific key path. The shema will tell + # you what type the keyPath expects, and other metadata about the config + # option. + # + # * `keyPath` The {String} name of the key. + # + # Returns an {Object} eg. `{type: 'integer', default: 23, minimum: 1}`. + # Returns `null` when the keyPath has no schema specified. getSchema: (keyPath) -> keys = keyPath.split('.') schema = @schema From 6b4ce902baebaafd8a397f1a99ebcd7fb00bc42c Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 12:37:02 -0700 Subject: [PATCH 055/145] Undefined in Config::set always unsets the value --- spec/config-spec.coffee | 17 +++++++++++++++++ src/config.coffee | 9 +++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 4e7aad526..4c006a24e 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -577,6 +577,23 @@ describe "Config", -> atom.config.set('foo.bar.anInt', 'cats') expect(atom.config.get('foo.bar.anInt')).toBe 'cats' + describe 'when the value has an "string" and "boolean" type', -> + beforeEach -> + schema = + type: ['string', 'boolean'] + default: 'def' + atom.config.setSchema('foo.bar', schema) + + it 'can set a string, a boolean, and unset', -> + atom.config.set('foo.bar', 'ok') + expect(atom.config.get('foo.bar')).toBe 'ok' + + atom.config.set('foo.bar', false) + expect(atom.config.get('foo.bar')).toBe false + + atom.config.set('foo.bar', undefined) + expect(atom.config.get('foo.bar')).toBe 'def' + describe 'when the value has a "number" type', -> beforeEach -> schema = diff --git a/src/config.coffee b/src/config.coffee index d5fe28dd5..930d2d419 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -381,10 +381,11 @@ class Config # * `true` if the value was set. # * `false` if the value was not able to be coerced to the type specified in the setting's schema. set: (keyPath, value) -> - try - value = @scrubValue(keyPath, value) - catch e - return false + unless value == undefined + try + value = @scrubValue(keyPath, value) + catch e + return false if @get(keyPath) isnt value defaultValue = _.valueForKeyPath(@defaultSettings, keyPath) From 800dee09baa8e2098a3d6580fca11927abd94199 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 12:52:44 -0700 Subject: [PATCH 056/145] Make boolean schema validator a little tighter --- spec/config-spec.coffee | 10 +++++++--- src/config-default-schema.coffee | 8 ++++---- src/config.coffee | 16 +++++++++++----- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 4c006a24e..f7149e62d 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -645,14 +645,18 @@ describe "Config", -> atom.config.set('foo.bar.aBool', 'FALSE') expect(atom.config.get('foo.bar.aBool')).toBe false atom.config.set('foo.bar.aBool', 1) - expect(atom.config.get('foo.bar.aBool')).toBe true + expect(atom.config.get('foo.bar.aBool')).toBe false atom.config.set('foo.bar.aBool', 0) expect(atom.config.get('foo.bar.aBool')).toBe false atom.config.set('foo.bar.aBool', {}) - expect(atom.config.get('foo.bar.aBool')).toBe true + expect(atom.config.get('foo.bar.aBool')).toBe false atom.config.set('foo.bar.aBool', null) expect(atom.config.get('foo.bar.aBool')).toBe false + # unset + atom.config.set('foo.bar.aBool', undefined) + expect(atom.config.get('foo.bar.aBool')).toBe true + describe 'when the value has an "string" type', -> beforeEach -> schema = @@ -691,7 +695,7 @@ describe "Config", -> atom.config.set 'foo.bar', anInt: '23' nestedObject: - nestedBool: 't' + nestedBool: 'true' expect(atom.config.get('foo.bar')).toEqual anInt: 23 nestedObject: diff --git a/src/config-default-schema.coffee b/src/config-default-schema.coffee index 4dc96d1cd..602e6790a 100644 --- a/src/config-default-schema.coffee +++ b/src/config-default-schema.coffee @@ -100,14 +100,14 @@ module.exports = type: 'object' properties: eol: - type: ['string', 'boolean'] + type: ['boolean', 'string'] default: '\u00ac' space: - type: ['string', 'boolean'] + type: ['boolean', 'string'] default: '\u00b7' tab: - type: ['string', 'boolean'] + type: ['boolean', 'string'] default: '\u00bb' cr: - type: ['string', 'boolean'] + type: ['boolean', 'string'] default: '\u00a4' diff --git a/src/config.coffee b/src/config.coffee index 930d2d419..cdee9f68d 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -154,9 +154,8 @@ pathWatcher = require 'pathwatcher' # # #### boolean # -# Values will be coerced into a Boolean. `'true'` and `'t'` will be coerced into -# `true`. Numbers, arrays, objects, and anything else will be coerced via -# `!!value`. +# Values will be coerced into a Boolean. `'true'` and `'false'` will be coerced into +# a boolean. Numbers, arrays, objects, and anything else will not be coerced. # # ```coffee # config: @@ -647,9 +646,16 @@ Config.addSchemaValidators coercion: (keyPath, value, schema) -> switch typeof value when 'string' - value.toLowerCase() in ['true', 't'] + if value.toLowerCase() in ['true'] + true + else if value.toLowerCase() in ['false'] + false + else + throw new Error("Cannot coerce #{keyPath}, #{JSON.stringify(value)} must be a boolean or the string 'true' or 'false'") + when 'boolean' + value else - !!value + throw new Error("Cannot coerce #{keyPath}, #{JSON.stringify(value)} must be a boolean or the string 'true' or 'false'") 'string': coercion: (keyPath, value, schema) -> From 33d4ace8e9bda6d3328951aa9eae6ceeeb758a01 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 12:52:56 -0700 Subject: [PATCH 057/145] :memo: more docs for Config --- src/config.coffee | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/config.coffee b/src/config.coffee index cdee9f68d..e1188e24f 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -113,6 +113,23 @@ pathWatcher = require 'pathwatcher' # # ### Supported Types # +# The `type` keyword can be a string with any one of the following. You can also +# chain them by specifying multiple in an an array. For example +# +# ```coffee +# config: +# someSetting: +# type: ['boolean', 'integer'] +# default: 5 +# +# # Then +# atom.config.set('my-package.someSetting', 'true') +# atom.config.get('my-package.someSetting') # -> true +# +# atom.config.set('my-package.someSetting', '12') +# atom.config.get('my-package.someSetting') # -> 12 +# ``` +# # #### string # # Values must be a string. From 94d470002b3f0314d0da4711bfdfc6edc89819ff Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 14:01:14 -0700 Subject: [PATCH 058/145] Update doc strings --- src/config.coffee | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index e1188e24f..847a86e13 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -319,11 +319,11 @@ class Config # than {::onDidChange} in that it will immediately call your callback with the # current value of the config entry. # - # * `keyPath` The {String} name of the key to observe - # * `callback` The {Function} to call when the value of the key changes. - # The first argument will be the new value of the key and the - #   second argument will be an {Object} with a `previous` property - # that is the prior value of the key. + # * `keyPath` {String} name of the key to observe + # * `callback` {Function} to call when the value of the key changes. + # * `value` the new value of the key + # * `event` {Object} + # * `previous` the prior value of the key. # # Returns a {Disposable} with the following keys on which you can call # `.dispose()` to unsubscribe. @@ -341,11 +341,11 @@ class Config # Essential: Add a listener for changes to a given key path. # - # * `keyPath` The {String} name of the key to observe - # * `callback` The {Function} to call when the value of the key changes. - # The first argument will be the new value of the key and the - #   second argument will be an {Object} with a `previous` property - # that is the prior value of the key. + # * `keyPath` {String} name of the key to observe + # * `callback` {Function} to call when the value of the key changes. + # * `value` the new value of the key + # * `event` {Object} + # * `previous` the prior value of the key. # # Returns a {Disposable} with the following keys on which you can call # `.dispose()` to unsubscribe. From e607d45f0de317668c8b01febdce845fe58669d2 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 14:01:50 -0700 Subject: [PATCH 059/145] Remove instances of getPositiveInt() --- src/display-buffer.coffee | 2 +- src/tokenized-buffer.coffee | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 60430692b..0b15bb7c5 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -439,7 +439,7 @@ class DisplayBuffer extends Model getSoftWrapColumn: -> if atom.config.get('editor.softWrapAtPreferredLineLength') - Math.min(@getEditorWidthInChars(), atom.config.getPositiveInt('editor.preferredLineLength', @getEditorWidthInChars())) + Math.min(@getEditorWidthInChars(), atom.config.get('editor.preferredLineLength')) else @getEditorWidthInChars() diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 8872b6e01..07cd8fc36 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -25,7 +25,7 @@ class TokenizedBuffer extends Model constructor: ({@buffer, @tabLength, @invisibles}) -> @emitter = new Emitter - @tabLength ?= atom.config.getPositiveInt('editor.tabLength', 2) + @tabLength ?= atom.config.get('editor.tabLength') @subscribe atom.syntax.onDidAddGrammar(@grammarAddedOrUpdated) @subscribe atom.syntax.onDidUpdateGrammar(@grammarAddedOrUpdated) @@ -35,8 +35,7 @@ class TokenizedBuffer extends Model @subscribe @$tabLength.changes, (tabLength) => @retokenizeLines() - @subscribe atom.config.observe 'editor.tabLength', callNow: false, => - @setTabLength(atom.config.getPositiveInt('editor.tabLength', 2)) + @subscribe atom.config.onDidChange 'editor.tabLength', (value) => @setTabLength(value) @reloadGrammar() From 22fb5adda983f47c794bcea98d61e1f8d68b9fe6 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 14:06:04 -0700 Subject: [PATCH 060/145] Remove deprecated calls for `config.observe .. callNow: false` in core --- spec/config-spec.coffee | 2 +- src/display-buffer.coffee | 4 ++-- src/package-manager.coffee | 2 +- src/text-editor.coffee | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index f7149e62d..429ab2a1a 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -232,7 +232,7 @@ describe "Config", -> it "emits an updated event", -> updatedCallback = jasmine.createSpy('updated') - atom.config.observe('foo.bar.baz.a', callNow: false, updatedCallback) + atom.config.onDidChange('foo.bar.baz.a', updatedCallback) expect(updatedCallback.callCount).toBe 0 atom.config.setDefaults("foo.bar.baz", a: 2) expect(updatedCallback.callCount).toBe 1 diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 0b15bb7c5..fc465e5d7 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -59,10 +59,10 @@ class DisplayBuffer extends Model @subscribe @buffer.onDidUpdateMarkers @handleBufferMarkersUpdated @subscribe @buffer.onDidCreateMarker @handleBufferMarkerCreated - @subscribe atom.config.observe 'editor.preferredLineLength', callNow: false, => + @subscribe atom.config.onDidChange 'editor.preferredLineLength', => @updateWrappedScreenLines() if @isSoftWrapped() and atom.config.get('editor.softWrapAtPreferredLineLength') - @subscribe atom.config.observe 'editor.softWrapAtPreferredLineLength', callNow: false, => + @subscribe atom.config.onDidChange 'editor.softWrapAtPreferredLineLength', => @updateWrappedScreenLines() if @isSoftWrapped() @updateAllScreenLines() diff --git a/src/package-manager.coffee b/src/package-manager.coffee index 838eda68e..889a7bdaf 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -260,7 +260,7 @@ class PackageManager @disabledPackagesSubscription = null observeDisabledPackages: -> - @disabledPackagesSubscription ?= atom.config.observe 'core.disabledPackages', callNow: false, (disabledPackages, {previous}) => + @disabledPackagesSubscription ?= atom.config.onDidChange 'core.disabledPackages', (disabledPackages, {previous}) => packagesToEnable = _.difference(previous, disabledPackages) packagesToDisable = _.difference(disabledPackages, previous) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index b941f4af0..1e3eba11b 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -113,8 +113,8 @@ class TextEditor extends Model @emit 'scroll-left-changed', scrollLeft @emitter.emit 'did-change-scroll-left', scrollLeft - @subscribe atom.config.observe 'editor.showInvisibles', callNow: false, (show) => @updateInvisibles() - @subscribe atom.config.observe 'editor.invisibles', callNow: false, => @updateInvisibles() + @subscribe atom.config.onDidChange 'editor.showInvisibles', => @updateInvisibles() + @subscribe atom.config.onDidChange 'editor.invisibles', => @updateInvisibles() atom.workspace?.editorAdded(this) if registerEditor From 604158647a541218106d85108ff3227033849a97 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 14:29:28 -0700 Subject: [PATCH 061/145] line height can be a string or a number --- src/config-default-schema.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config-default-schema.coffee b/src/config-default-schema.coffee index 602e6790a..aff325075 100644 --- a/src/config-default-schema.coffee +++ b/src/config-default-schema.coffee @@ -45,7 +45,7 @@ module.exports = default: 16 minimum: 1 lineHeight: - type: 'string' + type: ['string', 'number'] default: 1.3 showInvisibles: type: 'boolean' From 8b39ce77b11e22ae904135386bee72038e86d599 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 14:30:08 -0700 Subject: [PATCH 062/145] =?UTF-8?q?We=E2=80=99ll=20always=20have=20validat?= =?UTF-8?q?ors=20for=20a=20type.=20No=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config.coffee | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index 847a86e13..ff7402d9a 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -287,12 +287,11 @@ class Config types = [types] unless Array.isArray(types) for type in types try - if filterFunctions = @schemaValidators[type] - filterFunctions = filterFunctions.concat(@schemaValidators['*']) - for filter in filterFunctions - value = filter.call(this, keyPath, value, schema) - error = null - break + filterFunctions = @schemaValidators[type].concat(@schemaValidators['*']) + for filter in filterFunctions + value = filter.call(this, keyPath, value, schema) + error = null + break catch e error = e From 452e34db904b9b75d7ca00efdbc9a7d9ccea8fd9 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 14:40:15 -0700 Subject: [PATCH 063/145] Remove deprecations for push / remove / unshift at keypath --- docs/advanced/configuration.md | 9 --------- src/config.coffee | 11 ++++------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/docs/advanced/configuration.md b/docs/advanced/configuration.md index afd9a2292..9831ad2a0 100644 --- a/docs/advanced/configuration.md +++ b/docs/advanced/configuration.md @@ -48,15 +48,6 @@ but you can programmatically write to it with `atom.config.set`: atom.config.set("core.showInvisibles", true) ``` -You should never mutate the value of a config key, because that would circumvent -the notification of observers. You can however use methods like `pushAtKeyPath`, -`unshiftAtKeyPath`, and `removeAtKeyPath` to manipulate mutable config values. - -```coffeescript -atom.config.pushAtKeyPath("core.disabledPackages", "wrap-guide") -atom.config.removeAtKeyPath("core.disabledPackages", "terminal") -``` - You can also use `setDefaults`, which will assign default values for keys that are always overridden by values assigned with `set`. Defaults are not written out to the the `config.json` file to prevent it from becoming cluttered. diff --git a/src/config.coffee b/src/config.coffee index ff7402d9a..361ba9acb 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -487,31 +487,28 @@ class Config unobserve: (keyPath) -> deprecate 'Config::unobserve no longer does anything. Call `.dispose()` on the object returned by Config::observe instead.' + ### + Section: Private + ### + pushAtKeyPath: (keyPath, value) -> - deprecate 'Please remove from your code. Config::pushAtKeyPath is going away. Please push the value onto the array, and call Config::set' arrayValue = @get(keyPath) ? [] result = arrayValue.push(value) @set(keyPath, arrayValue) result unshiftAtKeyPath: (keyPath, value) -> - deprecate 'Please remove from your code. Config::unshiftAtKeyPath is going away. Please unshift the value onto the array, and call Config::set' arrayValue = @get(keyPath) ? [] result = arrayValue.unshift(value) @set(keyPath, arrayValue) result removeAtKeyPath: (keyPath, value) -> - deprecate 'Please remove from your code. Config::removeAtKeyPath is going away. Please remove the value from the array, and call Config::set' arrayValue = @get(keyPath) ? [] result = _.remove(arrayValue, value) @set(keyPath, arrayValue) result - ### - Section: Private - ### - initializeConfigDirectory: (done) -> return if fs.existsSync(@configDirPath) From 3a8f842de31c245fb086e880ae12ac39483e976a Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 14:46:05 -0700 Subject: [PATCH 064/145] Remove uses of toggle --- src/text-editor-component.coffee | 4 ++-- src/workspace-view.coffee | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 8fb98662e..270404ef4 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -506,8 +506,8 @@ TextEditorComponent = React.createClass 'editor:move-line-down': -> editor.moveLineDown() 'editor:duplicate-lines': -> editor.duplicateLines() 'editor:join-lines': -> editor.joinLines() - 'editor:toggle-indent-guide': -> atom.config.toggle('editor.showIndentGuide') - 'editor:toggle-line-numbers': -> atom.config.toggle('editor.showLineNumbers') + 'editor:toggle-indent-guide': -> atom.config.set('editor.showIndentGuide', not atom.config.get('editor.showIndentGuide')) + 'editor:toggle-line-numbers': -> atom.config.set('editor.showLineNumbers', not atom.config.get('editor.showLineNumbers')) 'editor:scroll-to-cursor': -> editor.scrollToCursorPosition() 'benchmark:scroll': @runScrollBenchmark diff --git a/src/workspace-view.coffee b/src/workspace-view.coffee index f011d4818..255ec3b5b 100644 --- a/src/workspace-view.coffee +++ b/src/workspace-view.coffee @@ -145,11 +145,11 @@ class WorkspaceView extends View @command 'window:focus-pane-on-left', => @focusPaneViewOnLeft() @command 'window:focus-pane-on-right', => @focusPaneViewOnRight() @command 'window:save-all', => @saveAll() - @command 'window:toggle-invisibles', -> atom.config.toggle("editor.showInvisibles") + @command 'window:toggle-invisibles', -> atom.config.set("editor.showInvisibles", not atom.config.get("editor.showInvisibles")) @command 'window:log-deprecation-warnings', -> logDeprecationWarnings() @command 'window:toggle-auto-indent', -> - atom.config.toggle("editor.autoIndent") + atom.config.set("editor.autoIndent", not atom.config.get("editor.autoIndent")) @command 'pane:reopen-closed-item', => @getModel().reopenItem() From fcf2143e709806d27c56a90ca8937a1812ad97a3 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 14:50:52 -0700 Subject: [PATCH 065/145] isPlainObject --- src/config.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.coffee b/src/config.coffee index 361ba9acb..a895ad533 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -624,7 +624,7 @@ class Config extractDefaultsFromSchema: (schema) -> if schema.default? schema.default - else if schema.type is 'object' and schema.properties? and typeof schema.properties is "object" + else if schema.type is 'object' and schema.properties? and isPlainObject(schema.properties) defaults = {} properties = schema.properties or {} defaults[key] = @extractDefaultsFromSchema(value) for key, value of properties From 96207ffbdb7e5b7f9a153382022e4dac427a7ca1 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 14:51:15 -0700 Subject: [PATCH 066/145] Update error messages to read good --- src/config.coffee | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index a895ad533..66e0f1e8c 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -646,44 +646,44 @@ Config.addSchemaValidators 'integer': coercion: (keyPath, value, schema) -> value = parseInt(value) - throw new Error("Cannot set #{keyPath}, #{JSON.stringify(value)} cannot be coerced into an int") if isNaN(value) or not isFinite(value) + throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} cannot be coerced into an int") if isNaN(value) or not isFinite(value) value 'number': coercion: (keyPath, value, schema) -> value = parseFloat(value) - throw new Error("Cannot set #{keyPath}, #{JSON.stringify(value)} cannot be coerced into a number") if isNaN(value) or not isFinite(value) + throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} cannot be coerced into a number") if isNaN(value) or not isFinite(value) value 'boolean': coercion: (keyPath, value, schema) -> switch typeof value when 'string' - if value.toLowerCase() in ['true'] + if value.toLowerCase() is 'true' true - else if value.toLowerCase() in ['false'] + else if value.toLowerCase() is 'false' false else - throw new Error("Cannot coerce #{keyPath}, #{JSON.stringify(value)} must be a boolean or the string 'true' or 'false'") + throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be a boolean or the string 'true' or 'false'") when 'boolean' value else - throw new Error("Cannot coerce #{keyPath}, #{JSON.stringify(value)} must be a boolean or the string 'true' or 'false'") + throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be a boolean or the string 'true' or 'false'") 'string': coercion: (keyPath, value, schema) -> - throw new Error("Cannot set #{keyPath}, #{JSON.stringify(value)} must be a string") if typeof value isnt 'string' + throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be a string") if typeof value isnt 'string' value 'null': # null sort of isnt supported. It will just unset in this case coercion: (keyPath, value, schema) -> - throw new Error("Cannot set #{keyPath}, #{JSON.stringify(value)} must be null") unless value == null + throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be null") unless value == null value 'object': coercion: (keyPath, value, schema) -> - throw new Error("Cannot set #{keyPath}, #{JSON.stringify(value)} must be an object") unless isPlainObject(value) + throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be an object") unless isPlainObject(value) return value unless schema.properties? newValue = {} @@ -697,7 +697,7 @@ Config.addSchemaValidators 'array': coercion: (keyPath, value, schema) -> - throw new Error("Cannot set #{keyPath}, #{JSON.stringify(value)} must be an array") unless Array.isArray(value) + throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be an array") unless Array.isArray(value) itemSchema = schema.items if itemSchema? newValue = [] @@ -727,7 +727,7 @@ Config.addSchemaValidators # Using `isEqual` for possibility of placing enums on array and object schemas return value if _.isEqual(possibleValue, value) - throw new Error("Cannot set #{keyPath}, #{JSON.stringify(value)} is not one of #{JSON.stringify(possibleValues)}") + throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} is not one of #{JSON.stringify(possibleValues)}") isPlainObject = (value) -> _.isObject(value) and not _.isArray(value) and not _.isFunction(value) and not _.isString(value) From 38d2303857421149dada66a01452b98c55ce614c Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 17:06:52 -0700 Subject: [PATCH 067/145] Clean up docs in creating a package --- docs/creating-a-package.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/creating-a-package.md b/docs/creating-a-package.md index c2f74e164..f4f096380 100644 --- a/docs/creating-a-package.md +++ b/docs/creating-a-package.md @@ -323,9 +323,8 @@ extensions your grammar supports: ## Adding Configuration Settings -You can support config options in your package that the user can edit in the -settings view. So do this you specify a `config` key in your package main -specifying the configuration. +You can support config settings in your package that are editable in the +settings view. Specify a `config` key in your package main: ```coffeescript module.exports = @@ -340,7 +339,7 @@ module.exports = ``` To specify the configuration, we use [json schema][json-schema] which allows you -to specify your value, the type it should be, etc. +to indicate the type your value should be, its default, etc. See the [Config API Docs](https://atom.io/docs/api/latest/Config) for more details specifying your configuration. From 1408d6964141a6de9d22cd1b523dc9027e2c7ab5 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 17:07:05 -0700 Subject: [PATCH 068/145] Fix up message strings --- src/config.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index 66e0f1e8c..8a63b9e00 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -469,15 +469,15 @@ class Config getInt: (keyPath) -> deprecate '''Config::getInt is no longer necessary. Use ::get instead. Make sure the config option you are accessing has specified an `integer` - schema. See the configuration section of - https://atom.io/docs/latest/creating-a-package for more info.''' + schema. See the schema section of + https://atom.io/docs/api/latest/Config for more info.''' parseInt(@get(keyPath)) getPositiveInt: (keyPath, defaultValue=0) -> deprecate '''Config::getPositiveInt is no longer necessary. Use ::get instead. Make sure the config option you are accessing has specified an `integer` - schema with `minimum: 1`. See the configuration section of - https://atom.io/docs/latest/creating-a-package for more info.''' + schema with `minimum: 1`. See the schema section of + https://atom.io/docs/api/latest/Config for more info.''' Math.max(@getInt(keyPath), 0) or defaultValue toggle: (keyPath) -> From 11fad1bd12adc3b890489dda64899ad0640ed4d3 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 17:11:24 -0700 Subject: [PATCH 069/145] Moar :memo: --- docs/creating-a-package.md | 2 +- src/config.coffee | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/creating-a-package.md b/docs/creating-a-package.md index f4f096380..393eaafd5 100644 --- a/docs/creating-a-package.md +++ b/docs/creating-a-package.md @@ -338,7 +338,7 @@ module.exports = # ... ``` -To specify the configuration, we use [json schema][json-schema] which allows you +To define the configuration, we use [json schema][json-schema] which allows you to indicate the type your value should be, its default, etc. See the [Config API Docs](https://atom.io/docs/api/latest/Config) for more diff --git a/src/config.coffee b/src/config.coffee index 8a63b9e00..e9e048602 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -59,7 +59,7 @@ pathWatcher = require 'pathwatcher' # # ## Defining settings for your package # -# Specify a schema under a `config` key in your package main. +# Define a schema under a `config` key in your package main. # # ```coffee # module.exports = @@ -79,7 +79,7 @@ pathWatcher = require 'pathwatcher' # # ## Config Schemas # -# We use [json schema](json-schema.org) which allows you to specify your value, its +# We use [json schema](json-schema.org) which allows you to define your value's # default, the type it should be, etc. A simple example: # # ```coffee @@ -260,8 +260,8 @@ pathWatcher = require 'pathwatcher' # default: 4 # ``` # -# __Note__: You should strive to be so clear in your naming of the config -# setting that you do not need to specify a title or description! +# __Note__: You should strive to be so clear in your naming of the setting that +# you do not need to specify a title or description! # # ## Best practices # From 2c1fa19e27d0c58da102b8511fc5104ab959a288 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 17:11:47 -0700 Subject: [PATCH 070/145] Update spec strings --- spec/config-spec.coffee | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 429ab2a1a..2e1b29b6d 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -385,7 +385,7 @@ describe "Config", -> """ atom.config.loadUserConfig() - it "updates the config data based on the file contents", -> + it "updates the only the settings that have values matching the schema", -> expect(atom.config.get("foo.bar")).toBe 'baz' expect(atom.config.get("foo.int")).toBe 12 @@ -438,7 +438,7 @@ describe "Config", -> atom.config.set("hair", "blonde") expect(atom.config.save).toHaveBeenCalled() - describe "when there is a schema specified", -> + describe "when a schema is specified", -> schema = null describe '.setSchema(keyPath, schema)', -> @@ -584,7 +584,7 @@ describe "Config", -> default: 'def' atom.config.setSchema('foo.bar', schema) - it 'can set a string, a boolean, and unset', -> + it 'can set a string, a boolean, and revert back to the default', -> atom.config.set('foo.bar', 'ok') expect(atom.config.get('foo.bar')).toBe 'ok' @@ -653,7 +653,10 @@ describe "Config", -> atom.config.set('foo.bar.aBool', null) expect(atom.config.get('foo.bar.aBool')).toBe false - # unset + it 'reverts back to the default value when undefined is passed to set', -> + atom.config.set('foo.bar.aBool', 'false') + expect(atom.config.get('foo.bar.aBool')).toBe false + atom.config.set('foo.bar.aBool', undefined) expect(atom.config.get('foo.bar.aBool')).toBe true From ef19e925e95704b000094a914b2f62090d3676d7 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 17:24:58 -0700 Subject: [PATCH 071/145] Strings accept numbers too --- spec/config-spec.coffee | 19 ++++++++++++++----- src/config.coffee | 10 +++++++--- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 2e1b29b6d..1209a51dc 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -671,12 +671,21 @@ describe "Config", -> atom.config.set('foo.bar.aString', 'yep') expect(atom.config.get('foo.bar.aString')).toBe 'yep' - it 'will not set non-strings', -> - expect(atom.config.set('foo.bar.aString', null)).toBe false - expect(atom.config.get('foo.bar.aString')).toBe 'ok' + it 'will only set strings, numbers and booleans', -> + expect(atom.config.set('foo.bar.aString', 123)).toBe true + expect(atom.config.get('foo.bar.aString')).toBe '123' - expect(atom.config.set('foo.bar.aString', 123)).toBe false - expect(atom.config.get('foo.bar.aString')).toBe 'ok' + expect(atom.config.set('foo.bar.aString', true)).toBe false + expect(atom.config.get('foo.bar.aString')).toBe '123' + + expect(atom.config.set('foo.bar.aString', null)).toBe false + expect(atom.config.get('foo.bar.aString')).toBe '123' + + expect(atom.config.set('foo.bar.aString', [])).toBe false + expect(atom.config.get('foo.bar.aString')).toBe '123' + + expect(atom.config.set('foo.bar.aString', nope: 'nope')).toBe false + expect(atom.config.get('foo.bar.aString')).toBe '123' describe 'when the value has an "object" type', -> beforeEach -> diff --git a/src/config.coffee b/src/config.coffee index e9e048602..1f23d7a5d 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -390,7 +390,8 @@ class Config # This value is stored in Atom's internal configuration file. # # * `keyPath` The {String} name of the key. - # * `value` The value of the setting. + # * `value` The value of the setting. Passing `undefined` will revert the + # setting to the default value. # # Returns a {Boolean} # * `true` if the value was set. @@ -672,8 +673,11 @@ Config.addSchemaValidators 'string': coercion: (keyPath, value, schema) -> - throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be a string") if typeof value isnt 'string' - value + switch typeof value + when 'number', 'string' + value.toString() + else + throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be a string or number") 'null': # null sort of isnt supported. It will just unset in this case From 4e1d13ceeadc63ab2bcc77948feb7cc04cf6d90c Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 17:39:06 -0700 Subject: [PATCH 072/145] is plain object --- src/config.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.coffee b/src/config.coffee index 1f23d7a5d..8920b7ed7 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -604,7 +604,7 @@ class Config @emitter.emit 'did-change' setSchema: (keyPath, schema) -> - unless typeof schema is "object" + unless isPlainObject(schema) throw new Error("Error loading schema for #{keyPath}: schemas can only be objects!") unless typeof schema.type? From 04d045227a5c6f6c539f9c128a1f7381cb50eaed Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 17:39:40 -0700 Subject: [PATCH 073/145] rename to config-defaults --- src/atom.coffee | 2 +- src/{config-default-schema.coffee => config-defaults.coffee} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{config-default-schema.coffee => config-defaults.coffee} (100%) diff --git a/src/atom.coffee b/src/atom.coffee index 9c507928e..0eeec7768 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -603,7 +603,7 @@ class Atom extends Model @deserializeWorkspaceView() loadConfig: -> - @config.setSchema null, {type: 'object', properties: _.clone(require('./config-default-schema'))} + @config.setSchema null, {type: 'object', properties: _.clone(require('./config-defaults'))} @config.load() loadThemes: -> diff --git a/src/config-default-schema.coffee b/src/config-defaults.coffee similarity index 100% rename from src/config-default-schema.coffee rename to src/config-defaults.coffee From 804d0d9911ab29a3f926eb9cd03592150bfbe56f Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 17:42:41 -0700 Subject: [PATCH 074/145] Doc :lipstick: --- src/config.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index 8920b7ed7..f66f561aa 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -12,10 +12,10 @@ pathWatcher = require 'pathwatcher' # # An instance of this class is always available as the `atom.config` global. # -# ## Getting and setting config settings +# ## Getting and setting config settings. Note that with no value set, {::get} +# returns the setting's default value. # # ```coffee -# # With no value set, `::get` returns the setting's default value # atom.config.get('my-package.myKey') # -> 'defaultValue' # # atom.config.set('my-package.myKey', 'value') @@ -244,7 +244,7 @@ pathWatcher = require 'pathwatcher' # # #### title and description # -# The settings view will use the `title` and `description` keys display your +# The settings view will use the `title` and `description` keys to display your # config setting in a readable way. By default the settings view humanizes your # config key, so `someSetting` becomes `Some Setting`. In some cases, this is # confusing for users, and a more descriptive title is useful. @@ -260,7 +260,7 @@ pathWatcher = require 'pathwatcher' # default: 4 # ``` # -# __Note__: You should strive to be so clear in your naming of the setting that +# __Note__: You should strive to be so clear in your naming of the setting that # you do not need to specify a title or description! # # ## Best practices From 3977596084cbdb9a7ceb4478ca74386256001971 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 17:50:36 -0700 Subject: [PATCH 075/145] Validators -> enforcers --- src/config.coffee | 45 +++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index f66f561aa..8e22586e2 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -270,26 +270,27 @@ pathWatcher = require 'pathwatcher' module.exports = class Config EmitterMixin.includeInto(this) - @schemaValidators = {} + @schemaEnforcers = {} - @addSchemaValidator: (typeName, validatorFunction) -> - @schemaValidators[typeName] ?= [] - @schemaValidators[typeName].push(validatorFunction) + @addSchemaEnforcer: (typeName, enforcerFunction) -> + @schemaEnforcers[typeName] ?= [] + @schemaEnforcers[typeName].push(enforcerFunction) - @addSchemaValidators: (filters) -> + @addSchemaEnforcers: (filters) -> for typeName, functions of filters - for name, validatorFunction of functions - @addSchemaValidator(typeName, validatorFunction) + for name, enforcerFunction of functions + @addSchemaEnforcer(typeName, enforcerFunction) - @executeSchemaValidators: (keyPath, value, schema) -> + @executeSchemaEnforcers: (keyPath, value, schema) -> error = null types = schema.type types = [types] unless Array.isArray(types) for type in types try - filterFunctions = @schemaValidators[type].concat(@schemaValidators['*']) - for filter in filterFunctions - value = filter.call(this, keyPath, value, schema) + enforcerFunctions = @schemaEnforcers[type].concat(@schemaEnforcers['*']) + for enforcer in enforcerFunctions + # At some point in one's life, one must call upon an enforcer. + value = enforcer.call(this, keyPath, value, schema) error = null break catch e @@ -399,7 +400,7 @@ class Config set: (keyPath, value) -> unless value == undefined try - value = @scrubValue(keyPath, value) + value = @makeValueConformToSchema(keyPath, value) catch e return false @@ -580,7 +581,7 @@ class Config @setRecursive(keys.concat([key]).join('.'), childValue) else try - value = @scrubValue(keyPath, value) + value = @makeValueConformToSchema(keyPath, value) defaultValue = _.valueForKeyPath(@defaultSettings, keyPath) value = undefined if _.isEqual(defaultValue, value) _.setValueForKeyPath(@settings, keyPath, value) @@ -631,19 +632,19 @@ class Config defaults[key] = @extractDefaultsFromSchema(value) for key, value of properties defaults - scrubValue: (keyPath, value) -> - value = @constructor.executeSchemaValidators(keyPath, value, schema) if schema = @getSchema(keyPath) + makeValueConformToSchema: (keyPath, value) -> + value = @constructor.executeSchemaEnforcers(keyPath, value, schema) if schema = @getSchema(keyPath) value -# Base schema validators. These will coerce raw input into the specified type, +# Base schema enforcers. These will coerce raw input into the specified type, # and will throw an error when the value cannot be coerced. Throwing the error # will indicate that the value should not be set. # -# Validators are run from most specific to least. For a schema with type -# `integer`, all the validators for the `integer` type will be run first, in -# order of specification. Then the `*` validators will be run, in order of +# Enforcers are run from most specific to least. For a schema with type +# `integer`, all the enforcers for the `integer` type will be run first, in +# order of specification. Then the `*` enforcers will be run, in order of # specification. -Config.addSchemaValidators +Config.addSchemaEnforcers 'integer': coercion: (keyPath, value, schema) -> value = parseInt(value) @@ -694,7 +695,7 @@ Config.addSchemaValidators for prop, childSchema of schema.properties continue unless value.hasOwnProperty(prop) try - newValue[prop] = @executeSchemaValidators("#{keyPath}.#{prop}", value[prop], childSchema) + newValue[prop] = @executeSchemaEnforcers("#{keyPath}.#{prop}", value[prop], childSchema) catch error console.warn "Error setting item in object: #{error.message}" newValue @@ -707,7 +708,7 @@ Config.addSchemaValidators newValue = [] for item in value try - newValue.push @executeSchemaValidators(keyPath, item, itemSchema) + newValue.push @executeSchemaEnforcers(keyPath, item, itemSchema) catch error console.warn "Error setting item in array: #{error.message}" newValue From 1f7aee00ac918dd7f77b596fd6213f283488a2ca Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 25 Sep 2014 17:51:34 -0700 Subject: [PATCH 076/145] function names to the imperative mood http://en.wikipedia.org/wiki/Imperative_mood --- src/config.coffee | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index 8e22586e2..136863bae 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -646,19 +646,19 @@ class Config # specification. Config.addSchemaEnforcers 'integer': - coercion: (keyPath, value, schema) -> + coerce: (keyPath, value, schema) -> value = parseInt(value) throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} cannot be coerced into an int") if isNaN(value) or not isFinite(value) value 'number': - coercion: (keyPath, value, schema) -> + coerce: (keyPath, value, schema) -> value = parseFloat(value) throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} cannot be coerced into a number") if isNaN(value) or not isFinite(value) value 'boolean': - coercion: (keyPath, value, schema) -> + coerce: (keyPath, value, schema) -> switch typeof value when 'string' if value.toLowerCase() is 'true' @@ -673,7 +673,7 @@ Config.addSchemaEnforcers throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be a boolean or the string 'true' or 'false'") 'string': - coercion: (keyPath, value, schema) -> + coerce: (keyPath, value, schema) -> switch typeof value when 'number', 'string' value.toString() @@ -682,12 +682,12 @@ Config.addSchemaEnforcers 'null': # null sort of isnt supported. It will just unset in this case - coercion: (keyPath, value, schema) -> + coerce: (keyPath, value, schema) -> throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be null") unless value == null value 'object': - coercion: (keyPath, value, schema) -> + coerce: (keyPath, value, schema) -> throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be an object") unless isPlainObject(value) return value unless schema.properties? @@ -701,7 +701,7 @@ Config.addSchemaEnforcers newValue 'array': - coercion: (keyPath, value, schema) -> + coerce: (keyPath, value, schema) -> throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be an array") unless Array.isArray(value) itemSchema = schema.items if itemSchema? @@ -716,7 +716,7 @@ Config.addSchemaEnforcers value '*': - minimumAndMaximumCoercion: (keyPath, value, schema) -> + coerceMinimumAndMaximum: (keyPath, value, schema) -> return value unless typeof value is 'number' if schema.minimum? and typeof schema.minimum is 'number' value = Math.max(value, schema.minimum) @@ -724,7 +724,7 @@ Config.addSchemaEnforcers value = Math.min(value, schema.maximum) value - enumValidation: (keyPath, value, schema) -> + validateEnum: (keyPath, value, schema) -> possibleValues = schema.enum return value unless possibleValues? and Array.isArray(possibleValues) and possibleValues.length From 08b138997d242baa4f53c577889dabe4567ac5b0 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 29 Sep 2014 14:37:52 -0700 Subject: [PATCH 077/145] Change the onDidChange / observe arguments Support passing no keypath --- spec/config-spec.coffee | 68 +++++++++++++++++++++++++++-------------- src/config.coffee | 28 ++++++++--------- 2 files changed, 59 insertions(+), 37 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 1209a51dc..703c037cb 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -48,7 +48,7 @@ describe "Config", -> atom.config.set("foo.bar.baz", 42) expect(atom.config.save).toHaveBeenCalled() - expect(observeHandler).toHaveBeenCalledWith 42, {previous: undefined} + expect(observeHandler).toHaveBeenCalledWith 42 describe "when the value equals the default value", -> it "does not store the value", -> @@ -139,7 +139,7 @@ describe "Config", -> expect(atom.config.pushAtKeyPath("foo.bar.baz", "b")).toBe 2 expect(atom.config.get("foo.bar.baz")).toEqual ["a", "b"] - expect(observeHandler).toHaveBeenCalledWith atom.config.get("foo.bar.baz"), {previous: ['a']} + expect(observeHandler).toHaveBeenCalledWith atom.config.get("foo.bar.baz") describe ".unshiftAtKeyPath(keyPath, value)", -> it "unshifts the given value to the array at the key path and updates observers", -> @@ -150,7 +150,7 @@ describe "Config", -> expect(atom.config.unshiftAtKeyPath("foo.bar.baz", "a")).toBe 2 expect(atom.config.get("foo.bar.baz")).toEqual ["a", "b"] - expect(observeHandler).toHaveBeenCalledWith atom.config.get("foo.bar.baz"), {previous: ['b']} + expect(observeHandler).toHaveBeenCalledWith atom.config.get("foo.bar.baz") describe ".removeAtKeyPath(keyPath, value)", -> it "removes the given value from the array at the key path and updates observers", -> @@ -161,7 +161,7 @@ describe "Config", -> expect(atom.config.removeAtKeyPath("foo.bar.baz", "b")).toEqual ["a", "c"] expect(atom.config.get("foo.bar.baz")).toEqual ["a", "c"] - expect(observeHandler).toHaveBeenCalledWith atom.config.get("foo.bar.baz"), {previous: ['a', 'b', 'c']} + expect(observeHandler).toHaveBeenCalledWith atom.config.get("foo.bar.baz") describe ".getPositiveInt(keyPath, defaultValue)", -> it "returns the proper coerced value", -> @@ -240,22 +240,45 @@ describe "Config", -> describe ".onDidChange(keyPath)", -> [observeHandler, observeSubscription] = [] - beforeEach -> - observeHandler = jasmine.createSpy("observeHandler") - atom.config.set("foo.bar.baz", "value 1") - observeSubscription = atom.config.onDidChange "foo.bar.baz", observeHandler + describe 'when a keyPath is specified', -> + beforeEach -> + observeHandler = jasmine.createSpy("observeHandler") + atom.config.set("foo.bar.baz", "value 1") + observeSubscription = atom.config.onDidChange "foo.bar.baz", observeHandler - it "does not fire the given callback with the current value at the keypath", -> - expect(observeHandler).not.toHaveBeenCalledWith("value 1") + it "does not fire the given callback with the current value at the keypath", -> + expect(observeHandler).not.toHaveBeenCalled() - it "fires the callback every time the observed value changes", -> - observeHandler.reset() # clear the initial call - atom.config.set('foo.bar.baz', "value 2") - expect(observeHandler).toHaveBeenCalledWith("value 2", {previous: 'value 1'}) - observeHandler.reset() + it "fires the callback every time the observed value changes", -> + observeHandler.reset() # clear the initial call + atom.config.set('foo.bar.baz', "value 2") + expect(observeHandler).toHaveBeenCalledWith({newValue: 'value 2', oldValue: 'value 1', keyPath: 'foo.bar.baz'}) + observeHandler.reset() - atom.config.set('foo.bar.baz', "value 1") - expect(observeHandler).toHaveBeenCalledWith("value 1", {previous: 'value 2'}) + atom.config.set('foo.bar.baz', "value 1") + expect(observeHandler).toHaveBeenCalledWith({newValue: 'value 1', oldValue: 'value 2', keyPath: 'foo.bar.baz'}) + + describe 'when a keyPath is not specified', -> + beforeEach -> + observeHandler = jasmine.createSpy("observeHandler") + atom.config.set("foo.bar.baz", "value 1") + observeSubscription = atom.config.onDidChange observeHandler + + it "does not fire the given callback initially", -> + expect(observeHandler).not.toHaveBeenCalled() + + it "fires the callback every time any value changes", -> + observeHandler.reset() # clear the initial call + atom.config.set('foo.bar.baz', "value 2") + expect(observeHandler).toHaveBeenCalledWith({newValue: 'value 2', oldValue: 'value 1', keyPath: 'foo.bar.baz'}) + + observeHandler.reset() + atom.config.set('foo.bar.baz', "value 1") + expect(observeHandler).toHaveBeenCalledWith({newValue: 'value 1', oldValue: 'value 2', keyPath: 'foo.bar.baz'}) + + observeHandler.reset() + atom.config.set('foo.bar.int', 1) + expect(observeHandler).toHaveBeenCalledWith({newValue: 1, oldValue: undefined, keyPath: 'foo.bar.int'}) describe ".observe(keyPath)", -> [observeHandler, observeSubscription] = [] @@ -271,26 +294,25 @@ describe "Config", -> it "fires the callback every time the observed value changes", -> observeHandler.reset() # clear the initial call atom.config.set('foo.bar.baz', "value 2") - expect(observeHandler).toHaveBeenCalledWith("value 2", {previous: 'value 1'}) + expect(observeHandler).toHaveBeenCalledWith("value 2") observeHandler.reset() atom.config.set('foo.bar.baz', "value 1") - expect(observeHandler).toHaveBeenCalledWith("value 1", {previous: 'value 2'}) + expect(observeHandler).toHaveBeenCalledWith("value 1") it "fires the callback when the observed value is deleted", -> observeHandler.reset() # clear the initial call atom.config.set('foo.bar.baz', undefined) - expect(observeHandler).toHaveBeenCalledWith(undefined, {previous: 'value 1'}) + expect(observeHandler).toHaveBeenCalledWith(undefined) it "fires the callback when the full key path goes into and out of existence", -> observeHandler.reset() # clear the initial call atom.config.set("foo.bar", undefined) + expect(observeHandler).toHaveBeenCalledWith(undefined) - expect(observeHandler).toHaveBeenCalledWith(undefined, {previous: 'value 1'}) observeHandler.reset() - atom.config.set("foo.bar.baz", "i'm back") - expect(observeHandler).toHaveBeenCalledWith("i'm back", {previous: undefined}) + expect(observeHandler).toHaveBeenCalledWith("i'm back") it "does not fire the callback once the observe subscription is off'ed", -> observeHandler.reset() # clear the initial call diff --git a/src/config.coffee b/src/config.coffee index 136863bae..a801bb910 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -333,11 +333,12 @@ class Config options = {} else message = "" - message = "`callNow` was set to false. Use ::onDidChange instead." if options.callNow == false + message = "`callNow` was set to false. Use ::onDidChange instead. Note that ::onDidChange calls back with different arguments." if options.callNow == false deprecate "Config::observe no longer supports options. #{message}" callback(_.clone(@get(keyPath))) unless options.callNow == false - @onDidChange(keyPath, callback) + @emitter.on 'did-change', (event) => + callback(event.newValue) if keyPath? and keyPath.indexOf(event?.keyPath) is 0 # Essential: Add a listener for changes to a given key path. # @@ -350,16 +351,12 @@ class Config # Returns a {Disposable} with the following keys on which you can call # `.dispose()` to unsubscribe. onDidChange: (keyPath, callback) -> - value = @get(keyPath) - previousValue = _.clone(value) - updateCallback = => - value = @get(keyPath) - unless _.isEqual(value, previousValue) - previous = previousValue - previousValue = _.clone(value) - callback(value, {previous}) + if arguments.length is 1 + callback = keyPath + keyPath = undefined - @emitter.on 'did-change', updateCallback + @emitter.on 'did-change', (event) => + callback(event) if not keyPath? or (keyPath? and keyPath.indexOf(event?.keyPath) is 0) ### Section: Managing Settings @@ -407,8 +404,11 @@ class Config if @get(keyPath) isnt value defaultValue = _.valueForKeyPath(@defaultSettings, keyPath) value = undefined if _.isEqual(defaultValue, value) + + oldValue = _.clone(@get(keyPath)) _.setValueForKeyPath(@settings, keyPath, value) - @update() + newValue = @get(keyPath) + @update({oldValue, newValue, keyPath}) unless _.isEqual(oldValue, newValue) true # Extended: Restore the key path to its default value. @@ -560,11 +560,11 @@ class Config @watchSubscription?.close() @watchSubscription = null - update: -> + update: (event) -> return if @configFileHasErrors @save() @emit 'updated' - @emitter.emit 'did-change' + @emitter.emit 'did-change', event save: -> CSON.writeFileSync(@configFilePath, @settings) From 98290b31ab6de4275c262c0095561ee05a86548d Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 29 Sep 2014 15:04:02 -0700 Subject: [PATCH 078/145] Rework defaults and user loading to notify per path --- src/config.coffee | 67 ++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index a801bb910..7a9f99666 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -401,14 +401,8 @@ class Config catch e return false - if @get(keyPath) isnt value - defaultValue = _.valueForKeyPath(@defaultSettings, keyPath) - value = undefined if _.isEqual(defaultValue, value) - - oldValue = _.clone(@get(keyPath)) - _.setValueForKeyPath(@settings, keyPath, value) - newValue = @get(keyPath) - @update({oldValue, newValue, keyPath}) unless _.isEqual(oldValue, newValue) + @setRawValue(keyPath, value) + @save() unless @configFileHasErrors true # Extended: Restore the key path to its default value. @@ -539,10 +533,8 @@ class Config try userConfig = CSON.readFileSync(@configFilePath) - @setAllRecursive(userConfig) + @setRecursive(null, userConfig) @configFileHasErrors = false - @emit 'updated' - @emitter.emit 'did-change' catch error @configFileHasErrors = true console.error "Failed to load user config '#{@configFilePath}'", error.message @@ -560,49 +552,52 @@ class Config @watchSubscription?.close() @watchSubscription = null - update: (event) -> - return if @configFileHasErrors - @save() - @emit 'updated' - @emitter.emit 'did-change', event - save: -> CSON.writeFileSync(@configFilePath, @settings) - setAllRecursive: (value) -> - @setRecursive(key, childValue) for key, childValue of value - return + setRawValue: (keyPath, value) -> + if @get(keyPath) isnt value + defaultValue = _.valueForKeyPath(@defaultSettings, keyPath) + value = undefined if _.isEqual(defaultValue, value) + + oldValue = _.clone(@get(keyPath)) + _.setValueForKeyPath(@settings, keyPath, value) + newValue = @get(keyPath) + @emitter.emit 'did-change', {oldValue, newValue, keyPath} + + setRawDefault: (keyPath, value) -> + oldValue = _.clone(@get(keyPath)) + _.setValueForKeyPath(@defaultSettings, keyPath, value) + newValue = @get(keyPath) + @emitter.emit 'did-change', {oldValue, newValue, keyPath} if newValue isnt oldValue setRecursive: (keyPath, value) -> if value? and isPlainObject(value) - keys = keyPath.split('.') + keys = if keyPath? then keyPath.split('.') else [] for key, childValue of value continue unless value.hasOwnProperty(key) @setRecursive(keys.concat([key]).join('.'), childValue) else try value = @makeValueConformToSchema(keyPath, value) - defaultValue = _.valueForKeyPath(@defaultSettings, keyPath) - value = undefined if _.isEqual(defaultValue, value) - _.setValueForKeyPath(@settings, keyPath, value) + @setRawValue(keyPath, value) catch e console.warn("'#{keyPath}' could not be set. Attempted value: #{JSON.stringify(value)}; Schema: #{JSON.stringify(@getSchema(keyPath))}") return setDefaults: (keyPath, defaults) -> - unless isPlainObject(defaults) - return _.setValueForKeyPath(@defaultSettings, keyPath, defaults) - - hash = @defaultSettings - if keyPath - for key in keyPath.split('.') - hash[key] ?= {} - hash = hash[key] - - _.extend hash, defaults - @emit 'updated' - @emitter.emit 'did-change' + if defaults? and isPlainObject(defaults) + keys = if keyPath? then keyPath.split('.') else [] + for key, childValue of defaults + continue unless defaults.hasOwnProperty(key) + @setDefaults(keys.concat([key]).join('.'), childValue) + else + try + defaults = @makeValueConformToSchema(keyPath, defaults) + @setRawDefault(keyPath, defaults) + catch e + console.warn("'#{keyPath}' could not set the default. Attempted default: #{JSON.stringify(defaults)}; Schema: #{JSON.stringify(@getSchema(keyPath))}") setSchema: (keyPath, schema) -> unless isPlainObject(schema) From 16c7fd3d70c8aa09869ac9be6a297bef7c45fac2 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 29 Sep 2014 15:13:13 -0700 Subject: [PATCH 079/145] Add spec for update event on load --- spec/config-spec.coffee | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 703c037cb..3d7d3de1e 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -359,11 +359,19 @@ describe "Config", -> describe "when the config file contains valid cson", -> beforeEach -> fs.writeFileSync(atom.config.configFilePath, "foo: bar: 'baz'") - atom.config.loadUserConfig() it "updates the config data based on the file contents", -> + atom.config.loadUserConfig() expect(atom.config.get("foo.bar")).toBe 'baz' + it "notifies observers for updated keypaths on load", -> + observeHandler = jasmine.createSpy("observeHandler") + observeSubscription = atom.config.observe "foo.bar", observeHandler + + atom.config.loadUserConfig() + + expect(observeHandler).toHaveBeenCalledWith 'baz' + describe "when the config file contains invalid cson", -> beforeEach -> spyOn(console, 'error') From a7185a894f404fa847a8b47fad49a19dde97b69f Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 29 Sep 2014 15:13:20 -0700 Subject: [PATCH 080/145] Fix specs --- spec/config-spec.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 3d7d3de1e..80d613b7e 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -433,7 +433,7 @@ describe "Config", -> atom.config.loadUserConfig() atom.config.observeUserConfig() updatedHandler = jasmine.createSpy("updatedHandler") - atom.config.on 'updated', updatedHandler + atom.config.onDidChange updatedHandler afterEach -> atom.config.unobserveUserConfig() @@ -461,7 +461,7 @@ describe "Config", -> describe "when the config file subsequently changes again to contain valid cson", -> beforeEach -> - fs.writeFileSync(atom.config.configFilePath, "foo: bar: 'baz'") + fs.writeFileSync(atom.config.configFilePath, "foo: bar: 'newVal'") waitsFor 'update event', -> updatedHandler.callCount > 0 it "updates the config data and resumes saving", -> From 5651ebbb48fb740c738ebadea900cee082c5ede4 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 29 Sep 2014 15:15:55 -0700 Subject: [PATCH 081/145] always set, only emit when values differ --- src/config.coffee | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index 7a9f99666..749d6b1df 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -556,14 +556,13 @@ class Config CSON.writeFileSync(@configFilePath, @settings) setRawValue: (keyPath, value) -> - if @get(keyPath) isnt value - defaultValue = _.valueForKeyPath(@defaultSettings, keyPath) - value = undefined if _.isEqual(defaultValue, value) + defaultValue = _.valueForKeyPath(@defaultSettings, keyPath) + value = undefined if _.isEqual(defaultValue, value) - oldValue = _.clone(@get(keyPath)) - _.setValueForKeyPath(@settings, keyPath, value) - newValue = @get(keyPath) - @emitter.emit 'did-change', {oldValue, newValue, keyPath} + oldValue = _.clone(@get(keyPath)) + _.setValueForKeyPath(@settings, keyPath, value) + newValue = @get(keyPath) + @emitter.emit 'did-change', {oldValue, newValue, keyPath} if newValue isnt oldValue setRawDefault: (keyPath, value) -> oldValue = _.clone(@get(keyPath)) From 1b506673bb6d398aeb76eff73b4f2f4495249a00 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 29 Sep 2014 15:20:36 -0700 Subject: [PATCH 082/145] :memo: update --- src/config.coffee | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index 749d6b1df..c3a87cfbb 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -322,8 +322,6 @@ class Config # * `keyPath` {String} name of the key to observe # * `callback` {Function} to call when the value of the key changes. # * `value` the new value of the key - # * `event` {Object} - # * `previous` the prior value of the key. # # Returns a {Disposable} with the following keys on which you can call # `.dispose()` to unsubscribe. @@ -340,13 +338,15 @@ class Config @emitter.on 'did-change', (event) => callback(event.newValue) if keyPath? and keyPath.indexOf(event?.keyPath) is 0 - # Essential: Add a listener for changes to a given key path. + # Essential: Add a listener for changes to a given key path. If `keyPath` is + # not specified, your callback will be called on changes to any key. # - # * `keyPath` {String} name of the key to observe + # * `keyPath` (optional) {String} name of the key to observe # * `callback` {Function} to call when the value of the key changes. - # * `value` the new value of the key # * `event` {Object} - # * `previous` the prior value of the key. + # * `newValue` the new value of the key + # * `oldValue` the prior value of the key. + # * `keyPath` the keyPath of the changed key # # Returns a {Disposable} with the following keys on which you can call # `.dispose()` to unsubscribe. From 454f9c4c656f4c02a90dbdb773dd1ac12d851a16 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 29 Sep 2014 15:26:52 -0700 Subject: [PATCH 083/145] Rename config-defaults -> config-schema --- src/atom.coffee | 2 +- src/{config-defaults.coffee => config-schema.coffee} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{config-defaults.coffee => config-schema.coffee} (100%) diff --git a/src/atom.coffee b/src/atom.coffee index 0eeec7768..688bec833 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -603,7 +603,7 @@ class Atom extends Model @deserializeWorkspaceView() loadConfig: -> - @config.setSchema null, {type: 'object', properties: _.clone(require('./config-defaults'))} + @config.setSchema null, {type: 'object', properties: _.clone(require('./config-schema'))} @config.load() loadThemes: -> diff --git a/src/config-defaults.coffee b/src/config-schema.coffee similarity index 100% rename from src/config-defaults.coffee rename to src/config-schema.coffee From 33b25c73120cc151726b5aba1fe0f600459761d2 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 29 Sep 2014 15:31:24 -0700 Subject: [PATCH 084/145] Use new config callback arguments --- src/package-manager.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/package-manager.coffee b/src/package-manager.coffee index 889a7bdaf..58d69f9dc 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -260,9 +260,9 @@ class PackageManager @disabledPackagesSubscription = null observeDisabledPackages: -> - @disabledPackagesSubscription ?= atom.config.onDidChange 'core.disabledPackages', (disabledPackages, {previous}) => - packagesToEnable = _.difference(previous, disabledPackages) - packagesToDisable = _.difference(disabledPackages, previous) + @disabledPackagesSubscription ?= atom.config.onDidChange 'core.disabledPackages', ({newValue, oldValue}) => + packagesToEnable = _.difference(oldValue, newValue) + packagesToDisable = _.difference(newValue, oldValue) @deactivatePackage(packageName) for packageName in packagesToDisable when @getActivePackage(packageName) @activatePackage(packageName) for packageName in packagesToEnable From f3ed3dc357959114b69a5160981ba23011f5d38e Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 29 Sep 2014 15:37:34 -0700 Subject: [PATCH 085/145] Fix doc to match implementation --- src/config.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index c3a87cfbb..9b4ee7c4b 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -34,8 +34,8 @@ pathWatcher = require 'pathwatcher' # If you want a notification only when the value changes, use {::onDidChange}. # # ```coffee -# atom.config.onDidChange 'my-package.myKey', (newValue) -> -# console.log 'My configuration changed:', newValue +# atom.config.onDidChange 'my-package.myKey', ({newValue, oldValue}) -> +# console.log 'My configuration changed:', newValue, oldValue # ``` # # ### Value Coercion From 9808264b7fd7a277b405dab3bf0744c0ed2d2cf3 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 29 Sep 2014 15:37:43 -0700 Subject: [PATCH 086/145] Fix onDidChange usage --- src/tokenized-buffer.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 07cd8fc36..ae2fa559a 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -35,7 +35,7 @@ class TokenizedBuffer extends Model @subscribe @$tabLength.changes, (tabLength) => @retokenizeLines() - @subscribe atom.config.onDidChange 'editor.tabLength', (value) => @setTabLength(value) + @subscribe atom.config.onDidChange 'editor.tabLength', ({newValue}) => @setTabLength(newValue) @reloadGrammar() From b54deccfaebbd8449643da7518e2eb39d4fa1ee4 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 29 Sep 2014 15:50:54 -0700 Subject: [PATCH 087/145] String type must be strict. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It makes sense to coerce from more general -> more specific data types. eg. string -> int, etc. But coercing the other way is problematic in the case of chaining because the more general type will swallow the specific type. eg. Setting `false` on type: [‘string’, ‘boolean’] will coerce the boolean to a string, and will never allow the value to be a boolean. --- spec/config-spec.coffee | 14 +++++++------- src/config.coffee | 10 ++++------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 80d613b7e..450564ea2 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -701,21 +701,21 @@ describe "Config", -> atom.config.set('foo.bar.aString', 'yep') expect(atom.config.get('foo.bar.aString')).toBe 'yep' - it 'will only set strings, numbers and booleans', -> - expect(atom.config.set('foo.bar.aString', 123)).toBe true - expect(atom.config.get('foo.bar.aString')).toBe '123' + it 'will only set strings', -> + expect(atom.config.set('foo.bar.aString', 123)).toBe false + expect(atom.config.get('foo.bar.aString')).toBe 'ok' expect(atom.config.set('foo.bar.aString', true)).toBe false - expect(atom.config.get('foo.bar.aString')).toBe '123' + expect(atom.config.get('foo.bar.aString')).toBe 'ok' expect(atom.config.set('foo.bar.aString', null)).toBe false - expect(atom.config.get('foo.bar.aString')).toBe '123' + expect(atom.config.get('foo.bar.aString')).toBe 'ok' expect(atom.config.set('foo.bar.aString', [])).toBe false - expect(atom.config.get('foo.bar.aString')).toBe '123' + expect(atom.config.get('foo.bar.aString')).toBe 'ok' expect(atom.config.set('foo.bar.aString', nope: 'nope')).toBe false - expect(atom.config.get('foo.bar.aString')).toBe '123' + expect(atom.config.get('foo.bar.aString')).toBe 'ok' describe 'when the value has an "object" type', -> beforeEach -> diff --git a/src/config.coffee b/src/config.coffee index 9b4ee7c4b..b2eb74f1e 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -667,12 +667,10 @@ Config.addSchemaEnforcers throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be a boolean or the string 'true' or 'false'") 'string': - coerce: (keyPath, value, schema) -> - switch typeof value - when 'number', 'string' - value.toString() - else - throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be a string or number") + validate: (keyPath, value, schema) -> + unless typeof value is 'string' + throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be a string") + value 'null': # null sort of isnt supported. It will just unset in this case From 443df292366f56985d31365412c35197d68e5d5c Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 29 Sep 2014 17:26:51 -0700 Subject: [PATCH 088/145] Upgrade find and replace to have cmd-d undo and skip --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e90d545d6..bf7c1be48 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "dev-live-reload": "0.34.0", "exception-reporting": "0.20.0", "feedback": "0.33.0", - "find-and-replace": "0.138.0", + "find-and-replace": "0.139.0", "fuzzy-finder": "0.58.0", "git-diff": "0.39.0", "go-to-line": "0.25.0", From 3efaeff669dc6fa811c4af94f2da429f19e8ff6c Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 30 Sep 2014 09:15:55 -0700 Subject: [PATCH 089/145] :apple: Install via move instead of copy This fixes the issue with the icon not showing up on OS X Mavericks when building. It seems that copying it to /Application file by file causes the icon to not show up while moving it atomically there does. --- build/package.json | 3 ++- build/tasks/install-task.coffee | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/build/package.json b/build/package.json index 422a48693..b81d226f5 100644 --- a/build/package.json +++ b/build/package.json @@ -8,7 +8,6 @@ "dependencies": { "async": "~0.2.9", "donna": "1.0.1", - "tello": "1.0.3", "formidable": "~1.0.14", "fs-plus": "2.x", "github-releases": "~0.2.0", @@ -35,6 +34,8 @@ "request": "~2.27.0", "rimraf": "~2.2.2", "runas": "~1.0.1", + "tello": "1.0.3", + "temp": "!0.8.1", "underscore-plus": "1.x", "unzip": "~0.1.9", "vm-compatibility-layer": "~0.1.0" diff --git a/build/tasks/install-task.coffee b/build/tasks/install-task.coffee index 13d349a50..650ac2372 100644 --- a/build/tasks/install-task.coffee +++ b/build/tasks/install-task.coffee @@ -3,6 +3,7 @@ path = require 'path' _ = require 'underscore-plus' fs = require 'fs-plus' runas = null +temp = require 'temp' module.exports = (grunt) -> {cp, mkdir, rm} = require('./task-helpers')(grunt) @@ -22,7 +23,11 @@ module.exports = (grunt) -> else if process.platform is 'darwin' rm installDir mkdir path.dirname(installDir) - cp shellAppDir, installDir + + tempFolder = temp.path() + mkdir tempFolder + cp shellAppDir, tempFolder + fs.renameSync(tempFolder, installDir) else binDir = path.join(installDir, 'bin') shareDir = path.join(installDir, 'share', 'atom') From 5a9b34b31a1b935b98e1a44e3a540ac868b87063 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 30 Sep 2014 09:32:03 -0700 Subject: [PATCH 090/145] ! -> ~ --- build/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/package.json b/build/package.json index b81d226f5..e25e02fc0 100644 --- a/build/package.json +++ b/build/package.json @@ -35,7 +35,7 @@ "rimraf": "~2.2.2", "runas": "~1.0.1", "tello": "1.0.3", - "temp": "!0.8.1", + "temp": "~0.8.1", "underscore-plus": "1.x", "unzip": "~0.1.9", "vm-compatibility-layer": "~0.1.0" From 50cf5f3e95ef922011f4c94b8ed7f42e15ba0a20 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 30 Sep 2014 09:33:50 -0700 Subject: [PATCH 091/145] Subscribe to editor commands We need to unsubscribe when the editor is removed! Closes #3651 --- src/text-editor-component.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 270404ef4..8519ac21d 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -514,8 +514,8 @@ TextEditorComponent = React.createClass addCommandListeners: (listenersByCommandName) -> {parentView} = @props - addListener = (command, listener) -> - parentView.command command, (event) -> + addListener = (command, listener) => + @subscribe parentView.command command, (event) -> event.stopPropagation() listener(event) From a8d93f9cf49dfb827f8119c24069d6d231e52cb1 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 30 Sep 2014 09:45:55 -0700 Subject: [PATCH 092/145] Spec for unsubscribing from commands --- spec/text-editor-component-spec.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 180efeeb5..b3f268b56 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -2264,6 +2264,10 @@ describe "TextEditorComponent", -> wrapperView.detach() wrapperView.attachToDom() + wrapperView.trigger('core:move-right') + + expect(editor.getCursorBufferPosition()).toEqual [0, 1] + buildMouseEvent = (type, properties...) -> properties = extend({bubbles: true, cancelable: true}, properties...) properties.detail ?= 1 From 504c4c7af642e14b39e34bddb24ecd7ff64ea276 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 29 Sep 2014 12:14:55 -0600 Subject: [PATCH 093/145] Extract MenuHelpers from MenuManager for reuse by ContextMenuManager --- src/menu-helpers.coffee | 32 ++++++++++++++++++++++++++++++++ src/menu-manager.coffee | 39 +++++++-------------------------------- 2 files changed, 39 insertions(+), 32 deletions(-) create mode 100644 src/menu-helpers.coffee diff --git a/src/menu-helpers.coffee b/src/menu-helpers.coffee new file mode 100644 index 000000000..156c83752 --- /dev/null +++ b/src/menu-helpers.coffee @@ -0,0 +1,32 @@ +merge = (menu, item) -> + matchingItem = findMatchingItem(menu, item) + + if matchingItem? + if item.submenu? + merge(matchingItem.submenu, submenuItem) for submenuItem in item.submenu + else + menu.push(item) + +unmerge = (menu, item) -> + if matchingItem = findMatchingItem(menu, item) + if item.submenu? + unmerge(matchingItem.submenu, submenuItem) for submenuItem in item.submenu + + unless matchingItem.submenu?.length > 0 + menu.splice(menu.indexOf(matchingItem), 1) + +findMatchingItem = (menu, {label, submenu}) -> + for item in menu + if normalizeLabel(item.label) is normalizeLabel(label) and item.submenu? is submenu? + return item + null + +normalizeLabel = (label) -> + return undefined unless label? + + if process.platform is 'darwin' + label + else + label.replace(/\&/g, '') + +module.exports = {merge, unmerge, findMatchingItem, normalizeLabel} diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee index 94d6fe5d3..d7190e80c 100644 --- a/src/menu-manager.coffee +++ b/src/menu-manager.coffee @@ -6,6 +6,8 @@ CSON = require 'season' fs = require 'fs-plus' {Disposable} = require 'event-kit' +MenuHelpers = require './menu-helpers' + # Extended: Provides a registry for menu items that you'd like to appear in the # application menu. # @@ -39,6 +41,7 @@ class MenuManager # Returns a {Disposable} on which `.dispose()` can be called to remove the # added menu items. add: (items) -> + items = _.deepClone(items) @merge(@template, item) for item in items @update() new Disposable => @remove(items) @@ -103,30 +106,10 @@ class MenuManager # Merges an item in a submenu aware way such that new items are always # appended to the bottom of existing menus where possible. merge: (menu, item) -> - item = _.deepClone(item) - matchingItem = @findMatchingItem(menu, item) - - if matchingItem? - if item.submenu? - @merge(matchingItem.submenu, submenuItem) for submenuItem in item.submenu - else - menu.push(item) + MenuHelpers.merge(menu, item) unmerge: (menu, item) -> - if matchingItem = @findMatchingItem(menu, item) - if item.submenu? - @unmerge(matchingItem.submenu, submenuItem) for submenuItem in item.submenu - - unless matchingItem.submenu?.length > 0 - menu.splice(menu.indexOf(matchingItem), 1) - - # find an existing menu item matching the given item - findMatchingItem: (menu, {label, submenu}) -> - debugger unless menu? - for item in menu - if @normalizeLabel(item.label) is @normalizeLabel(label) and item.submenu? is submenu? - return item - null + MenuHelpers.unmerge(menu, item) # OSX can't handle displaying accelerators for multiple keystrokes. # If they are sent across, it will stop processing accelerators for the rest @@ -145,25 +128,17 @@ class MenuManager keystrokesByCommand = @filterMultipleKeystroke(keystrokesByCommand) ipc.send 'update-application-menu', template, keystrokesByCommand - normalizeLabel: (label) -> - return undefined unless label? - - if process.platform is 'darwin' - label - else - label.replace(/\&/g, '') - # Get an {Array} of {String} classes for the given element. classesForElement: (element) -> element?.classList.toString().split(' ') ? [] sortPackagesMenu: -> - packagesMenu = @template.find ({label}) => @normalizeLabel(label) is 'Packages' + packagesMenu = @template.find ({label}) => MenuHelpers.normalizeLabel(label) is 'Packages' return unless packagesMenu?.submenu? packagesMenu.submenu.sort (item1, item2) => if item1.label and item2.label - @normalizeLabel(item1.label).localeCompare(@normalizeLabel(item2.label)) + MenuHelpers.normalizeLabel(item1.label).localeCompare(MenuHelpers.normalizeLabel(item2.label)) else 0 @update() From f8225a64418cf7c9eb5b961c23746f7e0bf916ea Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 29 Sep 2014 11:40:04 -0600 Subject: [PATCH 094/145] Make arguments atom.contextMenu.add consistent with atom.menu.add --- spec/context-menu-manager-spec.coffee | 218 +++++++++----------------- src/context-menu-manager.coffee | 158 +++++++++---------- src/menu-helpers.coffee | 5 +- 3 files changed, 153 insertions(+), 228 deletions(-) diff --git a/spec/context-menu-manager-spec.coffee b/spec/context-menu-manager-spec.coffee index 3cd2b28ff..07abb2da6 100644 --- a/spec/context-menu-manager-spec.coffee +++ b/spec/context-menu-manager-spec.coffee @@ -3,161 +3,97 @@ ContextMenuManager = require '../src/context-menu-manager' describe "ContextMenuManager", -> - [contextMenu] = [] + [contextMenu, parent, child, grandchild] = [] beforeEach -> {resourcePath} = atom.getLoadSettings() contextMenu = new ContextMenuManager({resourcePath}) - describe "adding definitions", -> - it 'loads', -> - contextMenu.add 'file-path', - '.selector': - 'label': 'command' + parent = document.createElement("div") + child = document.createElement("div") + grandchild = document.createElement("div") + parent.classList.add('parent') + child.classList.add('child') + grandchild.classList.add('grandchild') + child.appendChild(grandchild) + parent.appendChild(child) - expect(contextMenu.definitions['.selector'][0].label).toEqual 'label' - expect(contextMenu.definitions['.selector'][0].command).toEqual 'command' + describe "::add(itemsBySelector)", -> + it "can add top-level menu items that can be removed with the returned disposable", -> + disposable = contextMenu.add + '.parent': [{label: 'A', command: 'a'}] + '.child': [{label: 'B', command: 'b'}] + '.grandchild': [{label: 'C', command: 'c'}] - it 'does not add duplicate menu items', -> - contextMenu.add 'file-path', - '.selector': - 'label': 'command' + expect(contextMenu.templateForElement(grandchild)).toEqual [ + {label: 'C', command: 'c'} + {label: 'B', command: 'b'} + {label: 'A', command: 'a'} + ] - contextMenu.add 'file-path', - '.selector': - 'label': 'command' + disposable.dispose() + expect(contextMenu.templateForElement(grandchild)).toEqual [] - expect(contextMenu.definitions['.selector'][0].label).toEqual 'label' - expect(contextMenu.definitions['.selector'][0].command).toEqual 'command' - expect(contextMenu.definitions['.selector'].length).toBe 1 + it "can add submenu items to existing menus that can be removed with the returned disposable", -> + disposable1 = contextMenu.add + '.grandchild': [{label: 'A', submenu: [{label: 'B', command: 'b'}]}] + disposable2 = contextMenu.add + '.grandchild': [{label: 'A', submenu: [{label: 'C', command: 'c'}]}] - it 'allows multiple separators', -> - contextMenu.add 'file-path', - '.selector': - 'separator1': '-' - 'separator2': '-' - - expect(contextMenu.definitions['.selector'].length).toBe 2 - expect(contextMenu.definitions['.selector'][0].type).toEqual 'separator' - expect(contextMenu.definitions['.selector'][1].type).toEqual 'separator' - - it 'allows duplicate commands with different labels', -> - contextMenu.add 'file-path', - '.selector': - 'label': 'command' - - contextMenu.add 'file-path', - '.selector': - 'another label': 'command' - - expect(contextMenu.definitions['.selector'][0].label).toEqual 'label' - expect(contextMenu.definitions['.selector'][0].command).toEqual 'command' - expect(contextMenu.definitions['.selector'][1].label).toEqual 'another label' - expect(contextMenu.definitions['.selector'][1].command).toEqual 'command' - - it "loads submenus", -> - contextMenu.add 'file-path', - '.selector': - 'parent': - 'child-1': 'child-1:trigger' - 'child-2': 'child-2:trigger' - 'parent-2': 'parent-2:trigger' - - expect(contextMenu.definitions['.selector'].length).toBe 2 - expect(contextMenu.definitions['.selector'][0].label).toEqual 'parent' - expect(contextMenu.definitions['.selector'][0].submenu.length).toBe 2 - expect(contextMenu.definitions['.selector'][0].submenu[0].label).toBe 'child-1' - expect(contextMenu.definitions['.selector'][0].submenu[0].command).toBe 'child-1:trigger' - expect(contextMenu.definitions['.selector'][0].submenu[1].label).toBe 'child-2' - expect(contextMenu.definitions['.selector'][0].submenu[1].command).toBe 'child-2:trigger' - - describe 'dev mode', -> - it 'loads', -> - contextMenu.add 'file-path', - '.selector': - 'label': 'command' - , devMode: true - - expect(contextMenu.devModeDefinitions['.selector'][0].label).toEqual 'label' - expect(contextMenu.devModeDefinitions['.selector'][0].command).toEqual 'command' - - describe "building a menu template", -> - beforeEach -> - contextMenu.definitions = { - '.parent':[ - label: 'parent' - command: 'command-p' - ] - '.child': [ - label: 'child' - command: 'command-c' + expect(contextMenu.templateForElement(grandchild)).toEqual [{ + label: 'A', + submenu: [ + {label: 'C', command: 'c'} + {label: 'B', command: 'b'} ] - } + }] - contextMenu.devModeDefinitions = - '.parent': [ - label: 'dev-label' - command: 'dev-command' + disposable2.dispose() + expect(contextMenu.templateForElement(grandchild)).toEqual [{ + label: 'A', + submenu: [ + {label: 'B', command: 'b'} + ] + }] + + disposable1.dispose() + expect(contextMenu.templateForElement(grandchild)).toEqual [] + + it "favors the most specific / recently added item in the case of a duplicate label", -> + grandchild.classList.add('foo') + + disposable1 = contextMenu.add + '.grandchild': [{label: 'A', command: 'a'}] + disposable2 = contextMenu.add + '.grandchild.foo': [{label: 'A', command: 'b'}] + disposable3 = contextMenu.add + '.grandchild': [{label: 'A', command: 'c'}] + + expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'b'}] + + disposable2.dispose() + expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'c'}] + + disposable3.dispose() + expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'a'}] + + it "allows multiple separators", -> + contextMenu.add + '.grandchild': [ + {label: 'A', command: 'a'}, + {type: 'separator'}, + {label: 'B', command: 'b'}, + {type: 'separator'}, + {label: 'C', command: 'c'} ] - describe "on a single element", -> - [element] = [] - - beforeEach -> - element = ($$ -> @div class: 'parent')[0] - - it "creates a menu with a single item", -> - menu = contextMenu.combinedMenuTemplateForElement(element) - - expect(menu[0].label).toEqual 'parent' - expect(menu[0].command).toEqual 'command-p' - expect(menu[1]).toBeUndefined() - - describe "in devMode", -> - beforeEach -> contextMenu.devMode = true - - it "creates a menu with development items", -> - menu = contextMenu.combinedMenuTemplateForElement(element) - - expect(menu[0].label).toEqual 'parent' - expect(menu[0].command).toEqual 'command-p' - expect(menu[1].type).toEqual 'separator' - expect(menu[2].label).toEqual 'dev-label' - expect(menu[2].command).toEqual 'dev-command' - - - describe "on multiple elements", -> - [element] = [] - - beforeEach -> - element = $$ -> - @div class: 'parent', => - @div class: 'child' - - element = element.find('.child')[0] - - it "creates a menu with a two items", -> - menu = contextMenu.combinedMenuTemplateForElement(element) - - expect(menu[0].label).toEqual 'child' - expect(menu[0].command).toEqual 'command-c' - expect(menu[1].label).toEqual 'parent' - expect(menu[1].command).toEqual 'command-p' - expect(menu[2]).toBeUndefined() - - describe "in devMode", -> - beforeEach -> contextMenu.devMode = true - - xit "creates a menu with development items", -> - menu = contextMenu.combinedMenuTemplateForElement(element) - - expect(menu[0].label).toEqual 'child' - expect(menu[0].command).toEqual 'command-c' - expect(menu[1].label).toEqual 'parent' - expect(menu[1].command).toEqual 'command-p' - expect(menu[2].label).toEqual 'dev-label' - expect(menu[2].command).toEqual 'dev-command' - expect(menu[3]).toBeUndefined() + expect(contextMenu.templateForElement(grandchild)).toEqual [ + {label: 'A', command: 'a'}, + {type: 'separator'}, + {label: 'B', command: 'b'}, + {type: 'separator'}, + {label: 'C', command: 'c'} + ] describe "executeBuildHandlers", -> menuTemplate = [ diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index a55daabf0..f5c1f1471 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -4,6 +4,12 @@ remote = require 'remote' path = require 'path' CSON = require 'season' fs = require 'fs-plus' +{specificity} = require 'clear-cut' +{Disposable} = require 'event-kit' +MenuHelpers = require './menu-helpers' + +SpecificityCache = {} +SequenceCount = 0 # Extended: Provides a registry for commands that you'd like to appear in the # context menu. @@ -13,16 +19,17 @@ fs = require 'fs-plus' module.exports = class ContextMenuManager constructor: ({@resourcePath, @devMode}) -> - @definitions = {} - @devModeDefinitions = {} + @definitions = {'.overlayer': []} # TODO: Remove once color picker package stops touching private data @activeElement = null - @devModeDefinitions['.workspace'] = [ - label: 'Inspect Element' - command: 'application:inspect' - executeAtBuild: (e) -> - @commandOptions = x: e.pageX, y: e.pageY - ] + @itemSets = [] + + # @devModeDefinitions['.workspace'] = [ + # label: 'Inspect Element' + # command: 'application:inspect' + # executeAtBuild: (e) -> + # @commandOptions = x: e.pageX, y: e.pageY + # ] atom.keymaps.onDidLoadBundledKeymaps => @loadPlatformItems() @@ -41,91 +48,72 @@ class ContextMenuManager # * `devMode` Determines whether the entries should only be shown when # the window is in dev mode. add: (name, object, {devMode}={}) -> - for selector, items of object - for label, commandOrSubmenu of items + unless typeof arguments[0] is 'object' + return @add(@convertLegacyItems(object), {devMode}) + + itemsBySelector = _.deepClone(arguments[0]) + devMode = arguments[1]?.devMode ? false + addedItemSets = [] + + for selector, items of itemsBySelector + itemSet = new ContextMenuItemSet(selector, items) + addedItemSets.push(itemSet) + @itemSets.push(itemSet) + + new Disposable => + for itemSet in addedItemSets + @itemSets.splice(@itemSets.indexOf(itemSet), 1) + + templateForElement: (element) -> + template = [] + currentTarget = element + + while currentTarget? + matchingItemSets = + @itemSets + .filter (itemSet) -> currentTarget.webkitMatchesSelector(itemSet.selector) + .sort (a, b) -> a.compare(b) + + for {items} in matchingItemSets + MenuHelpers.merge(template, item) for item in items + + currentTarget = currentTarget.parentElement + + template + + convertLegacyItems: (legacyItems) -> + itemsBySelector = {} + + for selector, commandsByLabel of legacyItems + itemsBySelector[selector] = items = [] + + for label, commandOrSubmenu of commandsByLabel if typeof commandOrSubmenu is 'object' - submenu = [] - for submenuLabel, command of commandOrSubmenu - submenu.push(@buildMenuItem(submenuLabel, command)) - @addBySelector(selector, {label: label, submenu: submenu}, {devMode}) + items.push({label, submenu: @convertLegacyItems(commandOrSubmenu)}) + else if commandOrSubmenu is '-' + items.push({type: 'separator'}) else - menuItem = @buildMenuItem(label, commandOrSubmenu) - @addBySelector(selector, menuItem, {devMode}) + items.push({label, command: commandOrSubmenu}) - undefined - - buildMenuItem: (label, command) -> - if command is '-' - {type: 'separator'} - else - {label, command} - - # Registers a command to be displayed when the relevant item is right - # clicked. - # - # * `selector` The css selector for the active element which should include - # the given command in its context menu. - # * `definition` The object containing keys which match the menu template API. - # * `options` An optional {Object} with the following keys: - # * `devMode` Indicates whether this command should only appear while the - # editor is in dev mode. - addBySelector: (selector, definition, {devMode}={}) -> - definitions = if devMode then @devModeDefinitions else @definitions - if not _.findWhere(definitions[selector], definition) or _.isEqual(definition, {type: 'separator'}) - (definitions[selector] ?= []).push(definition) - - # Returns definitions which match the element and devMode. - definitionsForElement: (element, {devMode}={}) -> - definitions = if devMode then @devModeDefinitions else @definitions - matchedDefinitions = [] - for selector, items of definitions when element.webkitMatchesSelector(selector) - matchedDefinitions.push(_.clone(item)) for item in items - - matchedDefinitions - - # Used to generate the context menu for a specific element and it's - # parents. - # - # The menu items are sorted such that menu items that match closest to the - # active element are listed first. The further down the list you go, the higher - # up the ancestor hierarchy they match. - # - # * `element` The DOM element to generate the menu template for. - menuTemplateForMostSpecificElement: (element, {devMode}={}) -> - menuTemplate = @definitionsForElement(element, {devMode}) - if element.parentElement - menuTemplate.concat(@menuTemplateForMostSpecificElement(element.parentElement, {devMode})) - else - menuTemplate - - # Returns a menu template for both normal entries as well as - # development mode entries. - combinedMenuTemplateForElement: (element) -> - normalItems = @menuTemplateForMostSpecificElement(element) - devItems = if @devMode then @menuTemplateForMostSpecificElement(element, devMode: true) else [] - - menuTemplate = normalItems - menuTemplate.push({ type: 'separator' }) if normalItems.length > 0 and devItems.length > 0 - menuTemplate.concat(devItems) - - # Executes `executeAtBuild` if defined for each menu item with - # the provided event and then removes the `executeAtBuild` property from - # the menu item. - # - # This is useful for commands that need to provide data about the event - # to the command. - executeBuildHandlers: (event, menuTemplate) -> - for template in menuTemplate - template?.executeAtBuild?.call(template, event) - delete template.executeAtBuild + itemsBySelector # Public: Request a context menu to be displayed. # # * `event` A DOM event. showForEvent: (event) -> @activeElement = event.target - menuTemplate = @combinedMenuTemplateForElement(event.target) + menuTemplate = @templateForElement(@activeElement) + return unless menuTemplate?.length > 0 - @executeBuildHandlers(event, menuTemplate) + # @executeBuildHandlers(event, menuTemplate) remote.getCurrentWindow().emit('context-menu', menuTemplate) - undefined + return + +class ContextMenuItemSet + constructor: (@selector, @items) -> + @specificity = (SpecificityCache[@selector] ?= specificity(@selector)) + @sequenceNumber = SequenceCount++ + + compare: (other) -> + other.specificity - @specificity or + other.sequenceNumber - @sequenceNumber diff --git a/src/menu-helpers.coffee b/src/menu-helpers.coffee index 156c83752..e4d40bf81 100644 --- a/src/menu-helpers.coffee +++ b/src/menu-helpers.coffee @@ -15,11 +15,12 @@ unmerge = (menu, item) -> unless matchingItem.submenu?.length > 0 menu.splice(menu.indexOf(matchingItem), 1) -findMatchingItem = (menu, {label, submenu}) -> +findMatchingItem = (menu, {type, label, submenu}) -> + return if type is 'separator' for item in menu if normalizeLabel(item.label) is normalizeLabel(label) and item.submenu? is submenu? return item - null + return normalizeLabel = (label) -> return undefined unless label? From c5b395579bd7959f04e612210203a9f9291e8657 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 29 Sep 2014 15:24:30 -0600 Subject: [PATCH 095/145] Add devMode flag to individual items --- spec/context-menu-manager-spec.coffee | 9 +++++++++ src/context-menu-manager.coffee | 17 +++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/spec/context-menu-manager-spec.coffee b/spec/context-menu-manager-spec.coffee index 07abb2da6..e4a342f1e 100644 --- a/spec/context-menu-manager-spec.coffee +++ b/spec/context-menu-manager-spec.coffee @@ -95,6 +95,15 @@ describe "ContextMenuManager", -> {label: 'C', command: 'c'} ] + it "excludes items marked for display in devMode unless in dev mode", -> + disposable1 = contextMenu.add + '.grandchild': [{label: 'A', command: 'a', devMode: true}, {label: 'B', command: 'b', devMode: false}] + + expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'B', command: 'b'}] + + contextMenu.devMode = true + expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'a'}, {label: 'B', command: 'b'}] + describe "executeBuildHandlers", -> menuTemplate = [ label: 'label' diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index f5c1f1471..786455afc 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -47,9 +47,11 @@ class ContextMenuManager # * `options` An optional {Object} with the following keys: # * `devMode` Determines whether the entries should only be shown when # the window is in dev mode. - add: (name, object, {devMode}={}) -> + add: (items) -> unless typeof arguments[0] is 'object' - return @add(@convertLegacyItems(object), {devMode}) + legacyItems = arguments[1] + devMode = arguments[2]?.devMode + return @add(@convertLegacyItems(legacyItems, devMode)) itemsBySelector = _.deepClone(arguments[0]) devMode = arguments[1]?.devMode ? false @@ -75,13 +77,16 @@ class ContextMenuManager .sort (a, b) -> a.compare(b) for {items} in matchingItemSets - MenuHelpers.merge(template, item) for item in items + for item in items + continue if item.devMode and not @devMode + item = _.pick(item, 'type', 'label', 'command') + MenuHelpers.merge(template, item) currentTarget = currentTarget.parentElement template - convertLegacyItems: (legacyItems) -> + convertLegacyItems: (legacyItems, devMode) -> itemsBySelector = {} for selector, commandsByLabel of legacyItems @@ -89,11 +94,11 @@ class ContextMenuManager for label, commandOrSubmenu of commandsByLabel if typeof commandOrSubmenu is 'object' - items.push({label, submenu: @convertLegacyItems(commandOrSubmenu)}) + items.push({label, submenu: @convertLegacyItems(commandOrSubmenu, devMode), devMode}) else if commandOrSubmenu is '-' items.push({type: 'separator'}) else - items.push({label, command: commandOrSubmenu}) + items.push({label, command: commandOrSubmenu, devMode}) itemsBySelector From 3a567b3c5b790401b10e1964c92b676f314f64fa Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 29 Sep 2014 15:51:15 -0600 Subject: [PATCH 096/145] Call context menu item ::created hooks with the click event --- spec/context-menu-manager-spec.coffee | 18 ++++++++++++++ src/context-menu-manager.coffee | 35 +++++++++++++++------------ 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/spec/context-menu-manager-spec.coffee b/spec/context-menu-manager-spec.coffee index e4a342f1e..d26cbe175 100644 --- a/spec/context-menu-manager-spec.coffee +++ b/spec/context-menu-manager-spec.coffee @@ -104,6 +104,24 @@ describe "ContextMenuManager", -> contextMenu.devMode = true expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'a'}, {label: 'B', command: 'b'}] + it "allows items to be associated with `created` hooks which are invoked on template construction with the item and event", -> + createdEvent = null + + item = { + label: 'A', + command: 'a', + created: (event) -> + @command = 'b' + createdEvent = event + } + + contextMenu.add('.grandchild': [item]) + + dispatchedEvent = {target: grandchild} + expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual [{label: 'A', command: 'b'}] + expect(item.command).toBe 'a' # doesn't modify original item template + expect(createdEvent).toBe dispatchedEvent + describe "executeBuildHandlers", -> menuTemplate = [ label: 'label' diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index 786455afc..cefbeabde 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -19,17 +19,17 @@ SequenceCount = 0 module.exports = class ContextMenuManager constructor: ({@resourcePath, @devMode}) -> - @definitions = {'.overlayer': []} # TODO: Remove once color picker package stops touching private data @activeElement = null - @itemSets = [] + @definitions = {'.overlayer': []} # TODO: Remove once color picker package stops touching private data - # @devModeDefinitions['.workspace'] = [ - # label: 'Inspect Element' - # command: 'application:inspect' - # executeAtBuild: (e) -> - # @commandOptions = x: e.pageX, y: e.pageY - # ] + @add '.workspace': [{ + label: 'Inspect Element' + command: 'application:inspect' + created: (item, event) -> + {pageX, pageY} = event + item.commandOptions = {x: pageX, y: pageY} + }] atom.keymaps.onDidLoadBundledKeymaps => @loadPlatformItems() @@ -53,12 +53,12 @@ class ContextMenuManager devMode = arguments[2]?.devMode return @add(@convertLegacyItems(legacyItems, devMode)) - itemsBySelector = _.deepClone(arguments[0]) + itemsBySelector = arguments[0] devMode = arguments[1]?.devMode ? false addedItemSets = [] for selector, items of itemsBySelector - itemSet = new ContextMenuItemSet(selector, items) + itemSet = new ContextMenuItemSet(selector, items.slice()) addedItemSets.push(itemSet) @itemSets.push(itemSet) @@ -66,9 +66,12 @@ class ContextMenuManager for itemSet in addedItemSets @itemSets.splice(@itemSets.indexOf(itemSet), 1) - templateForElement: (element) -> + templateForElement: (target) -> + @templateForEvent({target}) + + templateForEvent: (event) -> template = [] - currentTarget = element + currentTarget = event.target while currentTarget? matchingItemSets = @@ -79,8 +82,10 @@ class ContextMenuManager for {items} in matchingItemSets for item in items continue if item.devMode and not @devMode - item = _.pick(item, 'type', 'label', 'command') - MenuHelpers.merge(template, item) + item = _.clone(item) + item.created?(event) + templateItem = _.pick(item, 'type', 'label', 'command', 'submenu', 'commandOptions') + MenuHelpers.merge(template, templateItem) currentTarget = currentTarget.parentElement @@ -107,7 +112,7 @@ class ContextMenuManager # * `event` A DOM event. showForEvent: (event) -> @activeElement = event.target - menuTemplate = @templateForElement(@activeElement) + menuTemplate = @templateForEvent(event) return unless menuTemplate?.length > 0 # @executeBuildHandlers(event, menuTemplate) From 782f9c609e2943f8fe7d61e6d7cfc738b1edc2b6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 29 Sep 2014 16:06:09 -0600 Subject: [PATCH 097/145] Add shouldDisplay hook for context menu items If present, if a falsy value is returned from this function for a given context menu invocation, the item will not be displayed. --- spec/context-menu-manager-spec.coffee | 31 ++++++++++++++++----------- src/context-menu-manager.coffee | 4 +++- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/spec/context-menu-manager-spec.coffee b/spec/context-menu-manager-spec.coffee index d26cbe175..79874bfe3 100644 --- a/spec/context-menu-manager-spec.coffee +++ b/spec/context-menu-manager-spec.coffee @@ -122,17 +122,24 @@ describe "ContextMenuManager", -> expect(item.command).toBe 'a' # doesn't modify original item template expect(createdEvent).toBe dispatchedEvent - describe "executeBuildHandlers", -> - menuTemplate = [ - label: 'label' - executeAtBuild: -> - ] - event = - target: null + it "allows items to be associated with `shouldDisplay` hooks which are invoked on construction to determine whether the item should be included", -> + shouldDisplayEvent = null + shouldDisplay = true - it 'should invoke the executeAtBuild fn', -> - buildFn = spyOn(menuTemplate[0], 'executeAtBuild') - contextMenu.executeBuildHandlers(event, menuTemplate) + item = { + label: 'A', + command: 'a', + shouldDisplay: (event) -> + @foo = 'bar' + shouldDisplayEvent = event + shouldDisplay + } + contextMenu.add('.grandchild': [item]) - expect(buildFn).toHaveBeenCalled() - expect(buildFn.mostRecentCall.args[0]).toBe event + dispatchedEvent = {target: grandchild} + expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual [{label: 'A', command: 'a'}] + expect(item.foo).toBeUndefined() # doesn't modify original item template + expect(shouldDisplayEvent).toBe dispatchedEvent + + shouldDisplay = false + expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual [] diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index cefbeabde..140361b60 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -82,7 +82,9 @@ class ContextMenuManager for {items} in matchingItemSets for item in items continue if item.devMode and not @devMode - item = _.clone(item) + item = Object.create(item) + if typeof item.shouldDisplay is 'function' + continue unless item.shouldDisplay(event) item.created?(event) templateItem = _.pick(item, 'type', 'label', 'command', 'submenu', 'commandOptions') MenuHelpers.merge(template, templateItem) From 2142c8e63e1480c9aa31c050df1e9d182c9fdacf Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 29 Sep 2014 16:24:33 -0600 Subject: [PATCH 098/145] :public: Document new ContextMenuManager::add API --- src/context-menu-manager.coffee | 50 ++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index 140361b60..6296ca34a 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -39,14 +39,50 @@ class ContextMenuManager map = CSON.readFileSync(platformMenuPath) atom.contextMenu.add(platformMenuPath, map['context-menu']) - # Public: Creates menu definitions from the object specified by the menu - # JSON API. + # Public: Add context menu items scoped by CSS selectors. # - # * `name` The path of the file that contains the menu definitions. - # * `object` The 'context-menu' object specified in the menu JSON API. - # * `options` An optional {Object} with the following keys: - # * `devMode` Determines whether the entries should only be shown when - # the window is in dev mode. + # ## Examples + # + # To add a context menu, pass a selector matching the elements to which you + # want the menu to apply as the top level key, followed by a menu descriptor. + # The invocation below adds a global 'Help' context menu item and a 'History' + # submenu on the editor supporting undo/redo. This is just for example + # purposes and not the way the menu is actually configured in Atom by default. + # + # ```coffee + # atom.contextMenu.add { + # '.workspace': [{label: 'Help', command: 'application:open-documentation'}] + # '.editor': [{ + # label: 'History', + # submenu: [ + # {label: 'Undo': command:'core:undo'} + # {label: 'Redo': command:'core:redo'} + # ] + # }] + # } + # ``` + # + # ## Arguments + # + # * `items` An {Object} whose keys are CSS selectors and whose values are + # {Array}s of item {Object}s containing the following keys: + # * `label` (Optional) A {String} containing the menu item's label. + # * `command` (Optional) A {String} containing the command to invoke on the + # target of the right click that invoked the context menu. + # * `submenu` (Optional) An {Array} of additional items. + # * `type` (Optional) If you want to create a separator, provide an item + # with `type: 'separator'` and no other keys. + # * `created` (Optional) A {Function} that is called on the item each time a + # context menu is created via a right click. You can assign properties to + # `this` to dynamically compute the command, label, etc. This method is + # actually called on a clone of the original item template to prevent state + # from leaking across context menu deployments. Called with the following + # argument: + # * `event` The click event that deployed the context menu. + # * `shouldDisplay` (Optional) A {Function} that is called to determine + # whether to display this item on a given context menu deployment. Called + # with the following argument: + # * `event` The click event that deployed the context menu. add: (items) -> unless typeof arguments[0] is 'object' legacyItems = arguments[1] From 703197bccaaef83c8986ffcf735634558c95fac8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 29 Sep 2014 16:26:11 -0600 Subject: [PATCH 099/145] Deprecate old style calls to ContextMenuManager::add --- src/context-menu-manager.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index 6296ca34a..c9f4f1e4f 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -6,6 +6,7 @@ CSON = require 'season' fs = require 'fs-plus' {specificity} = require 'clear-cut' {Disposable} = require 'event-kit' +Grim = require 'grim' MenuHelpers = require './menu-helpers' SpecificityCache = {} @@ -85,6 +86,7 @@ class ContextMenuManager # * `event` The click event that deployed the context menu. add: (items) -> unless typeof arguments[0] is 'object' + Grim.deprecate("ContextMenuManage::add has changed to take a single object as its argument. Please consult the documentation.") legacyItems = arguments[1] devMode = arguments[2]?.devMode return @add(@convertLegacyItems(legacyItems, devMode)) From aec6df828ea89ee3ce75f7e490896199cc707324 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 29 Sep 2014 16:28:31 -0600 Subject: [PATCH 100/145] fixup! Call context menu item ::created hooks with the click event --- src/context-menu-manager.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index c9f4f1e4f..8204f8087 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -27,9 +27,9 @@ class ContextMenuManager @add '.workspace': [{ label: 'Inspect Element' command: 'application:inspect' - created: (item, event) -> + created: (event) -> {pageX, pageY} = event - item.commandOptions = {x: pageX, y: pageY} + @commandOptions = {x: pageX, y: pageY} }] atom.keymaps.onDidLoadBundledKeymaps => @loadPlatformItems() From 483e7464394b78b1214f1935b141162fc4b8f419 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 29 Sep 2014 16:32:50 -0600 Subject: [PATCH 101/145] Use new format for platform menus --- menus/darwin.cson | 31 ++++++++++++++++--------------- menus/linux.cson | 31 ++++++++++++++++--------------- menus/win32.cson | 31 ++++++++++++++++--------------- src/context-menu-manager.coffee | 2 +- 4 files changed, 49 insertions(+), 46 deletions(-) diff --git a/menus/darwin.cson b/menus/darwin.cson index e73340a45..8d7ba5cdf 100644 --- a/menus/darwin.cson +++ b/menus/darwin.cson @@ -196,18 +196,19 @@ ] 'context-menu': - '.overlayer': - 'Undo': 'core:undo' - 'Redo': 'core:redo' - 'separator1': '-' - 'Cut': 'core:cut' - 'Copy': 'core:copy' - 'Paste': 'core:paste' - 'Delete': 'core:delete' - 'Select All': 'core:select-all' - 'separator2': '-' - 'Split Up': 'pane:split-up' - 'Split Down': 'pane:split-down' - 'Split Left': 'pane:split-left' - 'Split Right': 'pane:split-right' - 'separator3': '-' + '.editor': [ + {label: 'Undo', command: 'core:undo'} + {label: 'Redo', command: 'core:redo'} + {type: 'separator'} + {label: 'Cut', command: 'core:cut'} + {label: 'Copy', command: 'core:copy'} + {label: 'Paste', command: 'core:paste'} + {label: 'Delete', command: 'core:delete'} + {label: 'Select All', command: 'core:select-all'} + {type: 'separator'} + {label: 'Split Up', command: 'pane:split-up'} + {label: 'Split Down', command: 'pane:split-down'} + {label: 'Split Left', command: 'pane:split-left'} + {label: 'Split Right', command: 'pane:split-right'} + {type: 'separator'} + ] diff --git a/menus/linux.cson b/menus/linux.cson index 756d306d3..36f6c47da 100644 --- a/menus/linux.cson +++ b/menus/linux.cson @@ -153,18 +153,19 @@ ] 'context-menu': - '.overlayer': - 'Undo': 'core:undo' - 'Redo': 'core:redo' - 'separator1': '-' - 'Cut': 'core:cut' - 'Copy': 'core:copy' - 'Paste': 'core:paste' - 'Delete': 'core:delete' - 'Select All': 'core:select-all' - 'separator2': '-' - 'Split Up': 'pane:split-up' - 'Split Down': 'pane:split-down' - 'Split Left': 'pane:split-left' - 'Split Right': 'pane:split-right' - 'separator3': '-' + '.editor': [ + {label: 'Undo', command: 'core:undo'} + {label: 'Redo', command: 'core:redo'} + {type: 'separator'} + {label: 'Cut', command: 'core:cut'} + {label: 'Copy', command: 'core:copy'} + {label: 'Paste', command: 'core:paste'} + {label: 'Delete', command: 'core:delete'} + {label: 'Select All', command: 'core:select-all'} + {type: 'separator'} + {label: 'Split Up', command: 'pane:split-up'} + {label: 'Split Down', command: 'pane:split-down'} + {label: 'Split Left', command: 'pane:split-left'} + {label: 'Split Right', command: 'pane:split-right'} + {type: 'separator'} + ] diff --git a/menus/win32.cson b/menus/win32.cson index 96dbc2537..617e9c6c1 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -171,18 +171,19 @@ ] 'context-menu': - '.overlayer': - 'Undo': 'core:undo' - 'Redo': 'core:redo' - 'separator1': '-' - 'Cut': 'core:cut' - 'Copy': 'core:copy' - 'Paste': 'core:paste' - 'Delete': 'core:delete' - 'Select All': 'core:select-all' - 'separator2': '-' - 'Split Up': 'pane:split-up' - 'Split Down': 'pane:split-down' - 'Split Left': 'pane:split-left' - 'Split Right': 'pane:split-right' - 'separator3': '-' + '.editor': [ + {label: 'Undo', command: 'core:undo'} + {label: 'Redo', command: 'core:redo'} + {type: 'separator'} + {label: 'Cut', command: 'core:cut'} + {label: 'Copy', command: 'core:copy'} + {label: 'Paste', command: 'core:paste'} + {label: 'Delete', command: 'core:delete'} + {label: 'Select All', command: 'core:select-all'} + {type: 'separator'} + {label: 'Split Up', command: 'pane:split-up'} + {label: 'Split Down', command: 'pane:split-down'} + {label: 'Split Left', command: 'pane:split-left'} + {label: 'Split Right', command: 'pane:split-right'} + {type: 'separator'} + ] diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index 8204f8087..20a2a229c 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -38,7 +38,7 @@ class ContextMenuManager menusDirPath = path.join(@resourcePath, 'menus') platformMenuPath = fs.resolve(menusDirPath, process.platform, ['cson', 'json']) map = CSON.readFileSync(platformMenuPath) - atom.contextMenu.add(platformMenuPath, map['context-menu']) + atom.contextMenu.add(map['context-menu']) # Public: Add context menu items scoped by CSS selectors. # From 740778e129a1e2390d074f7ca321148ec70f4cb1 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 29 Sep 2014 16:43:31 -0600 Subject: [PATCH 102/145] Auto-detect context menu items in the old format --- src/context-menu-manager.coffee | 20 ++++++++++++-------- src/package.coffee | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index 20a2a229c..c0481bd5d 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -65,8 +65,8 @@ class ContextMenuManager # # ## Arguments # - # * `items` An {Object} whose keys are CSS selectors and whose values are - # {Array}s of item {Object}s containing the following keys: + # * `itemsBySelector` An {Object} whose keys are CSS selectors and whose + # values are {Array}s of item {Object}s containing the following keys: # * `label` (Optional) A {String} containing the menu item's label. # * `command` (Optional) A {String} containing the command to invoke on the # target of the right click that invoked the context menu. @@ -84,15 +84,19 @@ class ContextMenuManager # whether to display this item on a given context menu deployment. Called # with the following argument: # * `event` The click event that deployed the context menu. - add: (items) -> - unless typeof arguments[0] is 'object' + add: (itemsBySelector) -> + # Detect deprecated file path as first argument + unless typeof itemsBySelector is 'object' Grim.deprecate("ContextMenuManage::add has changed to take a single object as its argument. Please consult the documentation.") - legacyItems = arguments[1] + itemsBySelector = arguments[1] devMode = arguments[2]?.devMode - return @add(@convertLegacyItems(legacyItems, devMode)) - itemsBySelector = arguments[0] - devMode = arguments[1]?.devMode ? false + # Detect deprecated format for items object + for key, value of itemsBySelector + unless _.isArray(value) + Grim.deprecate("The format for declaring context menu items has changed. Please consult the documentation.") + itemsBySelector = @convertLegacyItems(itemsBySelector, devMode) + addedItemSets = [] for selector, items of itemsBySelector diff --git a/src/package.coffee b/src/package.coffee index dcb64056d..143629d38 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -163,7 +163,7 @@ class Package activateResources: -> atom.keymaps.add(keymapPath, map) for [keymapPath, map] in @keymaps - atom.contextMenu.add(menuPath, map['context-menu']) for [menuPath, map] in @menus + atom.contextMenu.add(map['context-menu']) for [menuPath, map] in @menus atom.menu.add(map.menu) for [menuPath, map] in @menus when map.menu unless @grammarsActivated From ff76e36f7df2df3393696f9c827199cecb756aa8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 29 Sep 2014 16:56:14 -0600 Subject: [PATCH 103/145] =?UTF-8?q?Only=20display=20=E2=80=98Inspect=20Ele?= =?UTF-8?q?ment=E2=80=99=20item=20in=20dev=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/context-menu-manager.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index c0481bd5d..dc0e81c27 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -27,6 +27,7 @@ class ContextMenuManager @add '.workspace': [{ label: 'Inspect Element' command: 'application:inspect' + devMode: true created: (event) -> {pageX, pageY} = event @commandOptions = {x: pageX, y: pageY} From f9bf42db6415c817599229581c7917a60d541e8b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 29 Sep 2014 19:53:18 -0600 Subject: [PATCH 104/145] Remove commented line --- src/context-menu-manager.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index dc0e81c27..e7ec6907f 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -160,7 +160,6 @@ class ContextMenuManager menuTemplate = @templateForEvent(event) return unless menuTemplate?.length > 0 - # @executeBuildHandlers(event, menuTemplate) remote.getCurrentWindow().emit('context-menu', menuTemplate) return From f082f93ead2957d2488f3719a45fcb0931fe43c4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 29 Sep 2014 19:59:58 -0600 Subject: [PATCH 105/145] Update specs for new ContextMenuManager API/behavior When selectors have the same specificity, menu items added *later* appear higher in the list. --- .../package-with-menus/menus/menu-1.cson | 7 ++++--- .../package-with-menus/menus/menu-2.cson | 5 +++-- .../package-with-menus/menus/menu-3.cson | 5 +++-- spec/package-manager-spec.coffee | 16 ++++++++-------- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/spec/fixtures/packages/package-with-menus/menus/menu-1.cson b/spec/fixtures/packages/package-with-menus/menus/menu-1.cson index 6cb447eb1..0c95f5972 100644 --- a/spec/fixtures/packages/package-with-menus/menus/menu-1.cson +++ b/spec/fixtures/packages/package-with-menus/menus/menu-1.cson @@ -1,7 +1,8 @@ 'menu': [ - { 'label': 'Second to Last' } + {'label': 'Second to Last'} ] 'context-menu': - '.test-1': - 'Menu item 1': 'command-1' + '.test-1': [ + {label: 'Menu item 1', command: 'command-1'} + ] diff --git a/spec/fixtures/packages/package-with-menus/menus/menu-2.cson b/spec/fixtures/packages/package-with-menus/menus/menu-2.cson index be2419a56..46c3fb9ef 100644 --- a/spec/fixtures/packages/package-with-menus/menus/menu-2.cson +++ b/spec/fixtures/packages/package-with-menus/menus/menu-2.cson @@ -3,5 +3,6 @@ ] 'context-menu': - '.test-1': - 'Menu item 2': 'command-2' + '.test-1': [ + {label: 'Menu item 2', command: 'command-2'} + ] diff --git a/spec/fixtures/packages/package-with-menus/menus/menu-3.cson b/spec/fixtures/packages/package-with-menus/menus/menu-3.cson index 932cd4d5b..445028db6 100644 --- a/spec/fixtures/packages/package-with-menus/menus/menu-3.cson +++ b/spec/fixtures/packages/package-with-menus/menus/menu-3.cson @@ -3,5 +3,6 @@ ] 'context-menu': - '.test-1': - 'Menu item 3': 'command-3' + '.test-1': [ + {label: 'Menu item 3', command: 'command-3'} + ] diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 964dc5894..326908fe8 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -230,30 +230,30 @@ describe "PackageManager", -> it "loads all the .cson/.json files in the menus directory", -> element = ($$ -> @div class: 'test-1')[0] - expect(atom.contextMenu.definitionsForElement(element)).toEqual [] + expect(atom.contextMenu.templateForElement(element)).toEqual [] atom.packages.activatePackage("package-with-menus") expect(atom.menu.template.length).toBe 2 expect(atom.menu.template[0].label).toBe "Second to Last" expect(atom.menu.template[1].label).toBe "Last" - expect(atom.contextMenu.definitionsForElement(element)[0].label).toBe "Menu item 1" - expect(atom.contextMenu.definitionsForElement(element)[1].label).toBe "Menu item 2" - expect(atom.contextMenu.definitionsForElement(element)[2].label).toBe "Menu item 3" + expect(atom.contextMenu.templateForElement(element)[0].label).toBe "Menu item 3" + expect(atom.contextMenu.templateForElement(element)[1].label).toBe "Menu item 2" + expect(atom.contextMenu.templateForElement(element)[2].label).toBe "Menu item 1" describe "when the metadata contains a 'menus' manifest", -> it "loads only the menus specified by the manifest, in the specified order", -> element = ($$ -> @div class: 'test-1')[0] - expect(atom.contextMenu.definitionsForElement(element)).toEqual [] + expect(atom.contextMenu.templateForElement(element)).toEqual [] atom.packages.activatePackage("package-with-menus-manifest") expect(atom.menu.template[0].label).toBe "Second to Last" expect(atom.menu.template[1].label).toBe "Last" - expect(atom.contextMenu.definitionsForElement(element)[0].label).toBe "Menu item 2" - expect(atom.contextMenu.definitionsForElement(element)[1].label).toBe "Menu item 1" - expect(atom.contextMenu.definitionsForElement(element)[2]).toBeUndefined() + expect(atom.contextMenu.templateForElement(element)[0].label).toBe "Menu item 1" + expect(atom.contextMenu.templateForElement(element)[1].label).toBe "Menu item 2" + expect(atom.contextMenu.templateForElement(element)[2]).toBeUndefined() describe "stylesheet loading", -> describe "when the metadata contains a 'stylesheets' manifest", -> From 915cfe15f5a92824be1197ebfd2f953732878ba7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 29 Sep 2014 20:25:55 -0600 Subject: [PATCH 106/145] Clear context menus between specs --- spec/spec-helper.coffee | 1 + src/context-menu-manager.coffee | 24 +++++++++++++----------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index a427f21a6..cc5f05c1f 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -125,6 +125,7 @@ beforeEach -> afterEach -> atom.packages.deactivatePackages() atom.menu.template = [] + atom.contextMenu.clear() atom.workspaceView?.remove?() atom.workspaceView = null diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index e7ec6907f..d982537e9 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -20,18 +20,8 @@ SequenceCount = 0 module.exports = class ContextMenuManager constructor: ({@resourcePath, @devMode}) -> - @activeElement = null - @itemSets = [] @definitions = {'.overlayer': []} # TODO: Remove once color picker package stops touching private data - - @add '.workspace': [{ - label: 'Inspect Element' - command: 'application:inspect' - devMode: true - created: (event) -> - {pageX, pageY} = event - @commandOptions = {x: pageX, y: pageY} - }] + @clear() atom.keymaps.onDidLoadBundledKeymaps => @loadPlatformItems() @@ -163,6 +153,18 @@ class ContextMenuManager remote.getCurrentWindow().emit('context-menu', menuTemplate) return + clear: -> + @activeElement = null + @itemSets = [] + @add '.workspace': [{ + label: 'Inspect Element' + command: 'application:inspect' + devMode: true + created: (event) -> + {pageX, pageY} = event + @commandOptions = {x: pageX, y: pageY} + }] + class ContextMenuItemSet constructor: (@selector, @items) -> @specificity = (SpecificityCache[@selector] ?= specificity(@selector)) From 36d5359ef4e3941ee2ffe0da35a0a9ef5835e48e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Sep 2014 10:49:52 -0600 Subject: [PATCH 107/145] Restore original context menu ordering Previously I used CSS specificity to order the most specific / recently added menu items for a given element *first* when building up the context menu. When a duplicate label was found for a given menu I would refrain from inserting it. Now instead I order things the opposite way. The most specific / recently added items come later and items with the same label are clobbered by later items. --- spec/context-menu-manager-spec.coffee | 2 +- spec/package-manager-spec.coffee | 8 +++---- src/context-menu-manager.coffee | 10 ++++---- src/menu-helpers.coffee | 33 +++++++++++++++++++-------- 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/spec/context-menu-manager-spec.coffee b/spec/context-menu-manager-spec.coffee index 79874bfe3..a0cb69485 100644 --- a/spec/context-menu-manager-spec.coffee +++ b/spec/context-menu-manager-spec.coffee @@ -43,8 +43,8 @@ describe "ContextMenuManager", -> expect(contextMenu.templateForElement(grandchild)).toEqual [{ label: 'A', submenu: [ - {label: 'C', command: 'c'} {label: 'B', command: 'b'} + {label: 'C', command: 'c'} ] }] diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 326908fe8..6f3a76f69 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -237,9 +237,9 @@ describe "PackageManager", -> expect(atom.menu.template.length).toBe 2 expect(atom.menu.template[0].label).toBe "Second to Last" expect(atom.menu.template[1].label).toBe "Last" - expect(atom.contextMenu.templateForElement(element)[0].label).toBe "Menu item 3" + expect(atom.contextMenu.templateForElement(element)[0].label).toBe "Menu item 1" expect(atom.contextMenu.templateForElement(element)[1].label).toBe "Menu item 2" - expect(atom.contextMenu.templateForElement(element)[2].label).toBe "Menu item 1" + expect(atom.contextMenu.templateForElement(element)[2].label).toBe "Menu item 3" describe "when the metadata contains a 'menus' manifest", -> it "loads only the menus specified by the manifest, in the specified order", -> @@ -251,8 +251,8 @@ describe "PackageManager", -> expect(atom.menu.template[0].label).toBe "Second to Last" expect(atom.menu.template[1].label).toBe "Last" - expect(atom.contextMenu.templateForElement(element)[0].label).toBe "Menu item 1" - expect(atom.contextMenu.templateForElement(element)[1].label).toBe "Menu item 2" + expect(atom.contextMenu.templateForElement(element)[0].label).toBe "Menu item 2" + expect(atom.contextMenu.templateForElement(element)[1].label).toBe "Menu item 1" expect(atom.contextMenu.templateForElement(element)[2]).toBeUndefined() describe "stylesheet loading", -> diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index d982537e9..ab0813c60 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -97,7 +97,9 @@ class ContextMenuManager new Disposable => for itemSet in addedItemSets + console.log "removing", itemSet, @itemSets.indexOf(itemSet) @itemSets.splice(@itemSets.indexOf(itemSet), 1) + console.log "remaining", @itemSets.slice() templateForElement: (target) -> @templateForEvent({target}) @@ -119,8 +121,7 @@ class ContextMenuManager if typeof item.shouldDisplay is 'function' continue unless item.shouldDisplay(event) item.created?(event) - templateItem = _.pick(item, 'type', 'label', 'command', 'submenu', 'commandOptions') - MenuHelpers.merge(template, templateItem) + MenuHelpers.merge(template, MenuHelpers.cloneMenuItem(item)) currentTarget = currentTarget.parentElement @@ -170,6 +171,7 @@ class ContextMenuItemSet @specificity = (SpecificityCache[@selector] ?= specificity(@selector)) @sequenceNumber = SequenceCount++ + # more specific / recent item sets sort later, because we clobber existing menu items compare: (other) -> - other.specificity - @specificity or - other.sequenceNumber - @sequenceNumber + @specificity - other.specificity or + @sequenceNumber - other.sequenceNumber diff --git a/src/menu-helpers.coffee b/src/menu-helpers.coffee index e4d40bf81..c7b449112 100644 --- a/src/menu-helpers.coffee +++ b/src/menu-helpers.coffee @@ -1,26 +1,34 @@ +_ = require 'underscore-plus' + merge = (menu, item) -> - matchingItem = findMatchingItem(menu, item) + matchingItemIndex = findMatchingItemIndex(menu, item) + matchingItem = menu[matchingItemIndex] unless matchingItemIndex is - 1 if matchingItem? if item.submenu? merge(matchingItem.submenu, submenuItem) for submenuItem in item.submenu + else + menu[matchingItemIndex] = item else menu.push(item) unmerge = (menu, item) -> - if matchingItem = findMatchingItem(menu, item) + matchingItemIndex = findMatchingItemIndex(menu, item) + matchingItem = menu[matchingItemIndex] unless matchingItemIndex is - 1 + + if matchingItem? if item.submenu? unmerge(matchingItem.submenu, submenuItem) for submenuItem in item.submenu unless matchingItem.submenu?.length > 0 - menu.splice(menu.indexOf(matchingItem), 1) + menu.splice(matchingItemIndex, 1) -findMatchingItem = (menu, {type, label, submenu}) -> - return if type is 'separator' - for item in menu +findMatchingItemIndex = (menu, {type, label, submenu}) -> + return -1 if type is 'separator' + for item, index in menu if normalizeLabel(item.label) is normalizeLabel(label) and item.submenu? is submenu? - return item - return + return index + -1 normalizeLabel = (label) -> return undefined unless label? @@ -30,4 +38,11 @@ normalizeLabel = (label) -> else label.replace(/\&/g, '') -module.exports = {merge, unmerge, findMatchingItem, normalizeLabel} + +cloneMenuItem = (item) -> + item = _.pick(item, 'type', 'label', 'command', 'submenu', 'commandOptions') + if item.submenu? + item.submenu = item.submenu.map (submenuItem) -> cloneMenuItem(submenuItem) + item + +module.exports = {merge, unmerge, normalizeLabel, cloneMenuItem} From 1187b50d90797d5cf09ad86c2ddc0ea9585e199f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Sep 2014 10:50:32 -0600 Subject: [PATCH 108/145] Put platform items back on .overlayer so they sort before package items --- menus/darwin.cson | 2 +- menus/linux.cson | 2 +- menus/win32.cson | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/menus/darwin.cson b/menus/darwin.cson index 8d7ba5cdf..75a2d87a2 100644 --- a/menus/darwin.cson +++ b/menus/darwin.cson @@ -196,7 +196,7 @@ ] 'context-menu': - '.editor': [ + '.overlayer': [ {label: 'Undo', command: 'core:undo'} {label: 'Redo', command: 'core:redo'} {type: 'separator'} diff --git a/menus/linux.cson b/menus/linux.cson index 36f6c47da..503137e5f 100644 --- a/menus/linux.cson +++ b/menus/linux.cson @@ -153,7 +153,7 @@ ] 'context-menu': - '.editor': [ + '.overlayer': [ {label: 'Undo', command: 'core:undo'} {label: 'Redo', command: 'core:redo'} {type: 'separator'} diff --git a/menus/win32.cson b/menus/win32.cson index 617e9c6c1..07e332b97 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -171,7 +171,7 @@ ] 'context-menu': - '.editor': [ + '.overlayer': [ {label: 'Undo', command: 'core:undo'} {label: 'Redo', command: 'core:redo'} {type: 'separator'} From cf80b92f9adc46a57a942646338cd58bd9f90fcb Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Sep 2014 11:14:26 -0600 Subject: [PATCH 109/145] Remove logging --- src/context-menu-manager.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index ab0813c60..f39cd8b18 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -97,9 +97,7 @@ class ContextMenuManager new Disposable => for itemSet in addedItemSets - console.log "removing", itemSet, @itemSets.indexOf(itemSet) @itemSets.splice(@itemSets.indexOf(itemSet), 1) - console.log "remaining", @itemSets.slice() templateForElement: (target) -> @templateForEvent({target}) From eb929cb7a205398c2aa6df37ca07ae66f876853e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Sep 2014 11:44:48 -0600 Subject: [PATCH 110/145] Honor item specificity while still preserving addition order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rather than using order to specify item precedence, we now construct a set of menu items for each element traversing upward from the target. When merging items for a given element, we pass the specificity to the merge function, which uses it to decide whether or not to clobber existing items. When assembling the overall menu, we don’t ever clobber to ensure that items added for elements closer to the target always win over items matching further up the tree. --- spec/context-menu-manager-spec.coffee | 5 +++++ src/context-menu-manager.coffee | 23 +++++++++-------------- src/menu-helpers.coffee | 12 ++++++++---- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/spec/context-menu-manager-spec.coffee b/spec/context-menu-manager-spec.coffee index a0cb69485..9c3c02175 100644 --- a/spec/context-menu-manager-spec.coffee +++ b/spec/context-menu-manager-spec.coffee @@ -68,6 +68,8 @@ describe "ContextMenuManager", -> '.grandchild.foo': [{label: 'A', command: 'b'}] disposable3 = contextMenu.add '.grandchild': [{label: 'A', command: 'c'}] + disposable4 = contextMenu.add + '.child': [{label: 'A', command: 'd'}] expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'b'}] @@ -77,6 +79,9 @@ describe "ContextMenuManager", -> disposable3.dispose() expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'a'}] + disposable1.dispose() + expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'd'}] + it "allows multiple separators", -> contextMenu.add '.grandchild': [ diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index f39cd8b18..1f00c5e7a 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -10,7 +10,6 @@ Grim = require 'grim' MenuHelpers = require './menu-helpers' SpecificityCache = {} -SequenceCount = 0 # Extended: Provides a registry for commands that you'd like to appear in the # context menu. @@ -91,7 +90,7 @@ class ContextMenuManager addedItemSets = [] for selector, items of itemsBySelector - itemSet = new ContextMenuItemSet(selector, items.slice()) + itemSet = new ContextMenuItemSet(selector, items) addedItemSets.push(itemSet) @itemSets.push(itemSet) @@ -107,19 +106,21 @@ class ContextMenuManager currentTarget = event.target while currentTarget? + currentTargetItems = [] matchingItemSets = - @itemSets - .filter (itemSet) -> currentTarget.webkitMatchesSelector(itemSet.selector) - .sort (a, b) -> a.compare(b) + @itemSets.filter (itemSet) -> currentTarget.webkitMatchesSelector(itemSet.selector) - for {items} in matchingItemSets - for item in items + for itemSet in matchingItemSets + for item in itemSet.items continue if item.devMode and not @devMode item = Object.create(item) if typeof item.shouldDisplay is 'function' continue unless item.shouldDisplay(event) item.created?(event) - MenuHelpers.merge(template, MenuHelpers.cloneMenuItem(item)) + MenuHelpers.merge(currentTargetItems, MenuHelpers.cloneMenuItem(item), itemSet.specificity) + + for item in currentTargetItems + MenuHelpers.merge(template, item, false) currentTarget = currentTarget.parentElement @@ -167,9 +168,3 @@ class ContextMenuManager class ContextMenuItemSet constructor: (@selector, @items) -> @specificity = (SpecificityCache[@selector] ?= specificity(@selector)) - @sequenceNumber = SequenceCount++ - - # more specific / recent item sets sort later, because we clobber existing menu items - compare: (other) -> - @specificity - other.specificity or - @sequenceNumber - other.sequenceNumber diff --git a/src/menu-helpers.coffee b/src/menu-helpers.coffee index c7b449112..a8fd26a50 100644 --- a/src/menu-helpers.coffee +++ b/src/menu-helpers.coffee @@ -1,14 +1,18 @@ _ = require 'underscore-plus' -merge = (menu, item) -> +ItemSpecificities = new WeakMap + +merge = (menu, item, itemSpecificity=Infinity) -> + ItemSpecificities.set(item, itemSpecificity) if itemSpecificity matchingItemIndex = findMatchingItemIndex(menu, item) matchingItem = menu[matchingItemIndex] unless matchingItemIndex is - 1 if matchingItem? if item.submenu? - merge(matchingItem.submenu, submenuItem) for submenuItem in item.submenu - else - menu[matchingItemIndex] = item + merge(matchingItem.submenu, submenuItem, itemSpecificity) for submenuItem in item.submenu + else if itemSpecificity + unless itemSpecificity < ItemSpecificities.get(matchingItem) + menu[matchingItemIndex] = item else menu.push(item) From 4a0c5aaa70c08ce1bd6da2db8213990dd0acb227 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Sep 2014 11:51:49 -0600 Subject: [PATCH 111/145] Prevent adjacent menu separators --- spec/context-menu-manager-spec.coffee | 4 +++- src/menu-helpers.coffee | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/context-menu-manager-spec.coffee b/spec/context-menu-manager-spec.coffee index 9c3c02175..821cb115d 100644 --- a/spec/context-menu-manager-spec.coffee +++ b/spec/context-menu-manager-spec.coffee @@ -82,13 +82,15 @@ describe "ContextMenuManager", -> disposable1.dispose() expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'd'}] - it "allows multiple separators", -> + it "allows multiple separators, but not adjacent to each other", -> contextMenu.add '.grandchild': [ {label: 'A', command: 'a'}, {type: 'separator'}, + {type: 'separator'}, {label: 'B', command: 'b'}, {type: 'separator'}, + {type: 'separator'}, {label: 'C', command: 'c'} ] diff --git a/src/menu-helpers.coffee b/src/menu-helpers.coffee index a8fd26a50..f360e7e9a 100644 --- a/src/menu-helpers.coffee +++ b/src/menu-helpers.coffee @@ -13,7 +13,7 @@ merge = (menu, item, itemSpecificity=Infinity) -> else if itemSpecificity unless itemSpecificity < ItemSpecificities.get(matchingItem) menu[matchingItemIndex] = item - else + else unless item.type is 'separator' and _.last(menu)?.type is 'separator' menu.push(item) unmerge = (menu, item) -> From f6938183cc135f4658bb251abb242ead278986f5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Sep 2014 11:53:24 -0600 Subject: [PATCH 112/145] Add pane splitting context menu items for all panes The same menu items remain for `.overlayer` to force them to be ordered before package context menu items. --- menus/darwin.cson | 8 ++++++++ menus/linux.cson | 8 ++++++++ menus/win32.cson | 8 ++++++++ 3 files changed, 24 insertions(+) diff --git a/menus/darwin.cson b/menus/darwin.cson index 75a2d87a2..f5bd9e168 100644 --- a/menus/darwin.cson +++ b/menus/darwin.cson @@ -212,3 +212,11 @@ {label: 'Split Right', command: 'pane:split-right'} {type: 'separator'} ] + '.pane': [ + {type: 'separator'} + {label: 'Split Up', command: 'pane:split-up'} + {label: 'Split Down', command: 'pane:split-down'} + {label: 'Split Left', command: 'pane:split-left'} + {label: 'Split Right', command: 'pane:split-right'} + {type: 'separator'} + ] diff --git a/menus/linux.cson b/menus/linux.cson index 503137e5f..d65536c10 100644 --- a/menus/linux.cson +++ b/menus/linux.cson @@ -169,3 +169,11 @@ {label: 'Split Right', command: 'pane:split-right'} {type: 'separator'} ] + '.pane': [ + {type: 'separator'} + {label: 'Split Up', command: 'pane:split-up'} + {label: 'Split Down', command: 'pane:split-down'} + {label: 'Split Left', command: 'pane:split-left'} + {label: 'Split Right', command: 'pane:split-right'} + {type: 'separator'} + ] diff --git a/menus/win32.cson b/menus/win32.cson index 07e332b97..8e094a8d8 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -187,3 +187,11 @@ {label: 'Split Right', command: 'pane:split-right'} {type: 'separator'} ] + '.pane': [ + {type: 'separator'} + {label: 'Split Up', command: 'pane:split-up'} + {label: 'Split Down', command: 'pane:split-down'} + {label: 'Split Left', command: 'pane:split-left'} + {label: 'Split Right', command: 'pane:split-right'} + {type: 'separator'} + ] From b2cc28fb5b6f25a83f68445239749a6a31a86e7b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Sep 2014 12:14:54 -0600 Subject: [PATCH 113/145] Rename commandOptions to commandDetail on context menu items --- src/browser/context-menu.coffee | 8 ++++---- src/context-menu-manager.coffee | 2 +- src/menu-helpers.coffee | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/browser/context-menu.coffee b/src/browser/context-menu.coffee index b3a043fda..2b4a8206e 100644 --- a/src/browser/context-menu.coffee +++ b/src/browser/context-menu.coffee @@ -13,12 +13,12 @@ class ContextMenu createClickHandlers: (template) -> for item in template if item.command - item.commandOptions ?= {} - item.commandOptions.contextCommand = true - item.commandOptions.atomWindow = @atomWindow + item.commandDetail ?= {} + item.commandDetail.contextCommand = true + item.commandDetail.atomWindow = @atomWindow do (item) => item.click = => - global.atomApplication.sendCommandToWindow(item.command, @atomWindow, item.commandOptions) + global.atomApplication.sendCommandToWindow(item.command, @atomWindow, item.commandDetail) else if item.submenu @createClickHandlers(item.submenu) item diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index 1f00c5e7a..5514cbb65 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -162,7 +162,7 @@ class ContextMenuManager devMode: true created: (event) -> {pageX, pageY} = event - @commandOptions = {x: pageX, y: pageY} + @commandDetail = {x: pageX, y: pageY} }] class ContextMenuItemSet diff --git a/src/menu-helpers.coffee b/src/menu-helpers.coffee index f360e7e9a..c47e9ae8b 100644 --- a/src/menu-helpers.coffee +++ b/src/menu-helpers.coffee @@ -44,7 +44,7 @@ normalizeLabel = (label) -> cloneMenuItem = (item) -> - item = _.pick(item, 'type', 'label', 'command', 'submenu', 'commandOptions') + item = _.pick(item, 'type', 'label', 'command', 'submenu', 'commandDetail') if item.submenu? item.submenu = item.submenu.map (submenuItem) -> cloneMenuItem(submenuItem) item From ff0a7be48ae12264f958c1d50388858140bd23d5 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 30 Sep 2014 11:18:45 -0700 Subject: [PATCH 114/145] Upgrade to settings-view@0.148 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bf7c1be48..3a9000fc2 100644 --- a/package.json +++ b/package.json @@ -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", From 8cb8f098035b575532f62961358a33cbddf7bb7d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Sep 2014 12:43:57 -0600 Subject: [PATCH 115/145] Return a Disposable instance from DeserializerManager::add --- spec/deserializer-manager-spec.coffee | 51 ++++++++++++++++----------- src/deserializer-manager.coffee | 4 +++ 2 files changed, 35 insertions(+), 20 deletions(-) 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/src/deserializer-manager.coffee b/src/deserializer-manager.coffee index 9abefc4c6..54b76c3c8 100644 --- a/src/deserializer-manager.coffee +++ b/src/deserializer-manager.coffee @@ -1,3 +1,5 @@ +{Disposable} = require 'event-kit' + # Extended: Manages the deserializers used for serialized state # # An instance of this class is always available as the `atom.deserializers` @@ -27,6 +29,8 @@ class DeserializerManager # * `classes` One or more classes to register. add: (classes...) -> @deserializers[klass.name] = klass for klass in classes + new Disposable => + delete @deserializers[klass.name] for klass in classes # Public: Remove the given class(es) as deserializers. # From 83710ed254fa9e22743cb4cde5d25cf5e8969203 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Sep 2014 12:47:56 -0600 Subject: [PATCH 116/145] :pencil: Rename classes param to deserializers and update docs --- src/deserializer-manager.coffee | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/deserializer-manager.coffee b/src/deserializer-manager.coffee index 54b76c3c8..8398b1379 100644 --- a/src/deserializer-manager.coffee +++ b/src/deserializer-manager.coffee @@ -26,11 +26,14 @@ 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[klass.name] for klass in classes + delete @deserializers[deserializer.name] for deserializer in deserializers # Public: Remove the given class(es) as deserializers. # From df1ae64f62f812a0d1d11b4e6bf60c6c234485dc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Sep 2014 12:48:58 -0600 Subject: [PATCH 117/145] Deprecate DeserializerManager::remove --- src/deserializer-manager.coffee | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/deserializer-manager.coffee b/src/deserializer-manager.coffee index 8398b1379..50becb31a 100644 --- a/src/deserializer-manager.coffee +++ b/src/deserializer-manager.coffee @@ -1,4 +1,5 @@ {Disposable} = require 'event-kit' +Grim = require 'grim' # Extended: Manages the deserializers used for serialized state # @@ -35,10 +36,8 @@ class DeserializerManager 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. From 33a5ca30dce5e76d26e671125cfe1b3489249b9c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Sep 2014 12:54:50 -0600 Subject: [PATCH 118/145] Use DeserializerManager::add disposable instead of ::remove in specs --- spec/pane-container-view-spec.coffee | 6 +++--- spec/pane-spec.coffee | 6 ++++-- spec/pane-view-spec.coffee | 6 +++--- 3 files changed, 10 insertions(+), 8 deletions(-) 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", -> From 51475fe2313bb77b5cd7840cf2327203c1893241 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Sep 2014 13:09:40 -0600 Subject: [PATCH 119/145] Upgrade first-mate to return Disposable from GrammarRegistry::addGrammar --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3a9000fc2..89ce3ffe7 100644 --- a/package.json +++ b/package.json @@ -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", From 54af7eced1a14aab1386e1e488256d8f2e20ee1a Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 30 Sep 2014 12:38:21 -0700 Subject: [PATCH 120/145] Handle empty config files + reset settings before applying user config Closes #3664 --- spec/config-spec.coffee | 29 +++++++++++++++++++++++++++++ src/config.coffee | 6 +++++- 2 files changed, 34 insertions(+), 1 deletion(-) 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/src/config.coffee b/src/config.coffee index b2eb74f1e..3f79fd8e8 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 userConfig + @setRecursive(null, userConfig) + else + @emitter.emit 'did-change' @configFileHasErrors = false catch error @configFileHasErrors = true From c74b1b971dcf662b7bb812f0b7c7a7aae3d898e7 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 30 Sep 2014 12:44:44 -0700 Subject: [PATCH 121/145] Use isPlainObject() --- src/config.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.coffee b/src/config.coffee index 3f79fd8e8..f2063cdf1 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -534,7 +534,7 @@ class Config try userConfig = CSON.readFileSync(@configFilePath) @settings = {} # Reset to the defaults - if userConfig + if isPlainObject(userConfig) @setRecursive(null, userConfig) else @emitter.emit 'did-change' From f205fe81ce241b69571c9272f392e83854de3e13 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Sep 2014 13:37:02 -0600 Subject: [PATCH 122/145] Actually update first-mate. Previous (51475fe231) updated atom-keymap. Both were needed to introduce disposables, but I mixed up the commit message in the previous commit. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 89ce3ffe7..a03aca027 100644 --- a/package.json +++ b/package.json @@ -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", From fd3cb1a23230bad423aa33695fecb46af7bf89ba Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Sep 2014 14:01:41 -0600 Subject: [PATCH 123/145] :lipstick: theme-manager-spec --- spec/theme-manager-spec.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee index 4a037d2f9..3ef9e21ba 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") From 211a1c75e2bf8baaa26e28e6febc31a8306e5a94 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Sep 2014 14:02:04 -0600 Subject: [PATCH 124/145] Return a disposable from ThemeManager::requireStylesheet --- spec/theme-manager-spec.coffee | 7 +++---- src/theme-manager.coffee | 5 ++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee index 3ef9e21ba..c7b64fec2 100644 --- a/spec/theme-manager-spec.coffee +++ b/spec/theme-manager-spec.coffee @@ -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/theme-manager.coffee b/src/theme-manager.coffee index 1aebe757b..4f748ca40 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' @@ -187,11 +187,10 @@ class ThemeManager 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 From 8f9f422406e007eb59144755e213bf59b60fd92c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Sep 2014 14:03:03 -0600 Subject: [PATCH 125/145] :pencil: Update return value docs --- src/theme-manager.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee index 4f748ca40..9ea248a26 100644 --- a/src/theme-manager.coffee +++ b/src/theme-manager.coffee @@ -182,7 +182,8 @@ 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) From 99a14c07f5241b83a07958282632324686abb60a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Sep 2014 14:13:50 -0600 Subject: [PATCH 126/145] Return a Disposable from Workspace::registerOpener --- src/workspace.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/workspace.coffee b/src/workspace.coffee index 65bf5895f..660244e12 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -5,7 +5,7 @@ _ = require 'underscore-plus' Q = require 'q' Serializable = require 'serializable' Delegator = require 'delegato' -{Emitter} = require 'event-kit' +{Emitter, Disposable} = require 'event-kit' TextEditor = require './text-editor' PaneContainer = require './pane-container' Pane = require './pane' @@ -363,11 +363,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: -> From 276102e19730aea961462d4ab4dcd5500a491a96 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Sep 2014 14:24:47 -0600 Subject: [PATCH 127/145] Require grim --- src/workspace.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/workspace.coffee b/src/workspace.coffee index 660244e12..1d890c217 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -6,6 +6,7 @@ Q = require 'q' Serializable = require 'serializable' Delegator = require 'delegato' {Emitter, Disposable} = require 'event-kit' +Grim = require 'grim' TextEditor = require './text-editor' PaneContainer = require './pane-container' Pane = require './pane' From 1f4359d4295c71e46188f80841b0ee0695ccc257 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 30 Sep 2014 13:25:55 -0700 Subject: [PATCH 128/145] Treat debugger statements as lint errors --- coffeelint.json | 3 +++ src/menu-manager.coffee | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) 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/src/menu-manager.coffee b/src/menu-manager.coffee index 94d6fe5d3..8417a5303 100644 --- a/src/menu-manager.coffee +++ b/src/menu-manager.coffee @@ -122,7 +122,6 @@ class MenuManager # find an existing menu item matching the given item findMatchingItem: (menu, {label, submenu}) -> - debugger unless menu? for item in menu if @normalizeLabel(item.label) is @normalizeLabel(label) and item.submenu? is submenu? return item From ef1e05fb89b7a0a6790d8bbc476eb620eee03231 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 30 Sep 2014 13:36:38 -0700 Subject: [PATCH 129/145] Prepare 0.134 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a03aca027..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": { From f84cb83e1ef25b022b99e23a868c0c9d675d4af4 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 30 Sep 2014 14:01:54 -0700 Subject: [PATCH 130/145] Use -> arrows --- src/config.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index f2063cdf1..96be654be 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -335,7 +335,7 @@ class Config deprecate "Config::observe no longer supports options. #{message}" callback(_.clone(@get(keyPath))) unless options.callNow == false - @emitter.on 'did-change', (event) => + @emitter.on 'did-change', (event) -> callback(event.newValue) if keyPath? and keyPath.indexOf(event?.keyPath) is 0 # Essential: Add a listener for changes to a given key path. If `keyPath` is @@ -355,7 +355,7 @@ class Config callback = keyPath keyPath = undefined - @emitter.on 'did-change', (event) => + @emitter.on 'did-change', (event) -> callback(event) if not keyPath? or (keyPath? and keyPath.indexOf(event?.keyPath) is 0) ### From 70a804bdb4d3f939fe207413d5d765caef96ac04 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 30 Sep 2014 17:09:35 -0600 Subject: [PATCH 131/145] Rename Workspace::registerOpener to ::addOpener for consistency --- spec/workspace-spec.coffee | 4 ++-- src/project.coffee | 4 ++-- src/workspace.coffee | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index 8e2d426f4..a175245a6 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -220,8 +220,8 @@ describe "Workspace", -> it "returns the resource returned by the custom opener", -> fooOpener = (pathToOpen, options) -> { foo: pathToOpen, options } if pathToOpen?.match(/\.foo/) barOpener = (pathToOpen) -> { bar: pathToOpen } if pathToOpen?.match(/^bar:\/\//) - workspace.registerOpener(fooOpener) - workspace.registerOpener(barOpener) + workspace.addOpener(fooOpener) + workspace.addOpener(barOpener) waitsForPromise -> pathToOpen = atom.project.resolve('a.foo') diff --git a/src/project.coffee b/src/project.coffee index 805afa49b..e57969436 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -345,12 +345,12 @@ class Project extends Model # Deprecated: delegate registerOpener: (opener) -> - deprecate("Use Workspace::registerOpener instead") + deprecate("Use Workspace::addOpener instead") atom.workspace.registerOpener(opener) # Deprecated: delegate unregisterOpener: (opener) -> - deprecate("Use Workspace::unregisterOpener instead") + deprecate("Call .dispose() on the Disposable returned from ::addOpener instead") atom.workspace.unregisterOpener(opener) # Deprecated: delegate diff --git a/src/workspace.coffee b/src/workspace.coffee index 1d890c217..a1420ea7f 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -44,7 +44,7 @@ class Workspace extends Model @paneContainer ?= new PaneContainer({@viewRegistry}) @paneContainer.onDidDestroyPaneItem(@onPaneItemDestroyed) - @registerOpener (filePath) => + @addOpener (filePath) => switch filePath when 'atom://.atom/stylesheet' @open(atom.themes.getUserStylesheetPath()) @@ -349,8 +349,6 @@ class Workspace extends Model if uri = @destroyedItemUris.pop() @openSync(uri) - # TODO: make ::registerOpener() return a disposable - # Public: Register an opener for a uri. # # An {TextEditor} will be used if no openers return a value. @@ -358,7 +356,7 @@ class Workspace extends Model # ## Examples # # ```coffee - # atom.project.registerOpener (uri) -> + # atom.project.addOpener (uri) -> # if path.extname(uri) is '.toml' # return new TomlEditor(uri) # ``` @@ -367,13 +365,15 @@ class Workspace extends Model # # Returns a {Disposable} on which `.dispose()` can be called to remove the # opener. - registerOpener: (opener) -> + addOpener: (opener) -> @openers.push(opener) new Disposable => _.remove(@openers, opener) + registerOpener: (opener) -> + Grim.deprecate("Call Workspace::addOpener instead") + @addOpener(opener) - # Unregister an opener registered with {::registerOpener}. unregisterOpener: (opener) -> - Grim.deprecate("Call .dispose() on the Disposable returned from ::registerOpener instead") + Grim.deprecate("Call .dispose() on the Disposable returned from ::addOpener instead") _.remove(@openers, opener) getOpeners: -> From a12fb94d77c3bcdad7b0ef0098a32a6b740d1b67 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 30 Sep 2014 16:09:48 -0700 Subject: [PATCH 132/145] Specific VCS in config title Closes atom/settings-view#41 --- src/config-schema.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config-schema.coffee b/src/config-schema.coffee index aff325075..8bd74e1a0 100644 --- a/src/config-schema.coffee +++ b/src/config-schema.coffee @@ -14,6 +14,7 @@ module.exports = excludeVcsIgnoredPaths: type: 'boolean' default: true + title: 'Exclude VCS Ignored Paths' disabledPackages: type: 'array' default: [] From ebf026def4b00dd2d68d52237dad1d1ba8d28667 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 30 Sep 2014 16:10:23 -0700 Subject: [PATCH 133/145] :memo: Make HEAD all caps in title --- src/config-schema.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config-schema.coffee b/src/config-schema.coffee index 8bd74e1a0..d841907cc 100644 --- a/src/config-schema.coffee +++ b/src/config-schema.coffee @@ -97,6 +97,7 @@ module.exports = confirmCheckoutHeadRevision: type: 'boolean' default: true + title: 'Confirm Checkout HEAD Revision' invisibles: type: 'object' properties: From df161d7d9be5e7c0cd3753245764e0640059ed3e Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 30 Sep 2014 16:57:06 -0700 Subject: [PATCH 134/145] Upgrade to settings-view@0.149 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4f5babcbd..88d2926f9 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "open-on-github": "0.30.0", "package-generator": "0.31.0", "release-notes": "0.36.0", - "settings-view": "0.148.0", + "settings-view": "0.149.0", "snippets": "0.53.0", "spell-check": "0.42.0", "status-bar": "0.45.0", From c66df2c05a8cd6bc825e6b85ab45ccae73847c07 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Wed, 1 Oct 2014 20:14:32 +0800 Subject: [PATCH 135/145] Upgrade to atom-shell@0.17.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 88d2926f9..98973961c 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "url": "http://github.com/atom/atom/raw/master/LICENSE.md" } ], - "atomShellVersion": "0.16.2", + "atomShellVersion": "0.17.0", "dependencies": { "async": "0.2.6", "atom-keymap": "^2.2.0", From 5e0c7d3a70c3da167ee6d93706c558dc27e421ef Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Wed, 1 Oct 2014 20:20:43 +0800 Subject: [PATCH 136/145] Upgrade to apm@0.98.0 --- apm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apm/package.json b/apm/package.json index ab1c265f3..ffbf9e421 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "0.97.0" + "atom-package-manager": "0.98.0" } } From bf19d098d5c163435320a61e248663861eaaafc9 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Wed, 1 Oct 2014 21:27:51 +0800 Subject: [PATCH 137/145] Upgrade to atom-shell@0.17.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 98973961c..c5fad5905 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "url": "http://github.com/atom/atom/raw/master/LICENSE.md" } ], - "atomShellVersion": "0.17.0", + "atomShellVersion": "0.17.1", "dependencies": { "async": "0.2.6", "atom-keymap": "^2.2.0", From fdb4cd7e53cbc285c892421c4b358036c1beb4e4 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Wed, 1 Oct 2014 21:37:50 +0800 Subject: [PATCH 138/145] Disable DirectWrite, fixes #3540 --- src/browser/atom-window.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/browser/atom-window.coffee b/src/browser/atom-window.coffee index c8e395720..bca8716ce 100644 --- a/src/browser/atom-window.coffee +++ b/src/browser/atom-window.coffee @@ -28,6 +28,7 @@ class AtomWindow title: 'Atom' icon: @constructor.iconPath 'web-preferences': + 'direct-write': false 'subpixel-font-scaling': false global.atomApplication.addWindow(this) From de434fcfbf836f30b5ffdf25788aa0ad28186d09 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 1 Oct 2014 08:31:58 -0700 Subject: [PATCH 139/145] Upgrade to fs-plus@2.3.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c5fad5905..8e9fb069f 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "emissary": "^1.3.1", "event-kit": "0.7.2", "first-mate": "^2.2.0", - "fs-plus": "^2.2.6", + "fs-plus": "^2.3.1", "fstream": "0.1.24", "fuzzaldrin": "^2.1", "git-utils": "^2.1.4", From 8806eef231cbde8dab5b52dce7b1856200e17503 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 1 Oct 2014 08:32:59 -0700 Subject: [PATCH 140/145] Upgrade to language-xml@0.22 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8e9fb069f..111f58c6a 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,7 @@ "language-text": "0.6.0", "language-todo": "0.12.0", "language-toml": "0.12.0", - "language-xml": "0.21.0", + "language-xml": "0.22.0", "language-yaml": "0.17.0" }, "private": true, From cd8c6690aa1bcae23c0e7703ae4dbbd4b70aad88 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 1 Oct 2014 08:40:08 -0700 Subject: [PATCH 141/145] Upgrade to image-view@0.37 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 111f58c6a..505885638 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "git-diff": "0.39.0", "go-to-line": "0.25.0", "grammar-selector": "0.34.0", - "image-view": "0.36.0", + "image-view": "0.37.0", "incompatible-packages": "0.9.0", "keybinding-resolver": "0.20.0", "link": "0.25.0", From 57603b3a00a80e78dca7f5aa9ae42466b5588cde Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 1 Oct 2014 09:37:25 -0700 Subject: [PATCH 142/145] Fix config resetting all values when one changes. Closes atom/settings-view#257 --- spec/config-spec.coffee | 26 ++++++++++++++++++++++++++ src/config.coffee | 31 ++++++++++++++++++++++--------- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 666c76b3d..0c9251054 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -448,6 +448,32 @@ describe "Config", -> expect(atom.config.get('foo.bar')).toBe 'quux' expect(atom.config.get('foo.baz')).toBe 'bar' + it "does not fire a change event for paths that did not change", -> + atom.config.onDidChange 'foo.bar', noChangeSpy = jasmine.createSpy() + + fs.writeFileSync(atom.config.configFilePath, "foo: { bar: 'baz', omg: 'ok'}") + waitsFor 'update event', -> updatedHandler.callCount > 0 + runs -> + expect(noChangeSpy).not.toHaveBeenCalled() + expect(atom.config.get('foo.bar')).toBe 'baz' + expect(atom.config.get('foo.omg')).toBe 'ok' + + describe 'when the default value is a complex value', -> + beforeEach -> + fs.writeFileSync(atom.config.configFilePath, "foo: { bar: ['baz', 'ok']}") + waitsFor 'update event', -> updatedHandler.callCount > 0 + runs -> updatedHandler.reset() + + it "does not fire a change event for paths that did not change", -> + atom.config.onDidChange 'foo.bar', noChangeSpy = jasmine.createSpy() + + fs.writeFileSync(atom.config.configFilePath, "foo: { bar: ['baz', 'ok'], omg: 'another'}") + waitsFor 'update event', -> updatedHandler.callCount > 0 + runs -> + expect(noChangeSpy).not.toHaveBeenCalled() + expect(atom.config.get('foo.bar')).toEqual ['baz', 'ok'] + expect(atom.config.get('foo.omg')).toBe 'another' + 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'}") diff --git a/src/config.coffee b/src/config.coffee index 96be654be..56d3efa9d 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -533,11 +533,7 @@ class Config try userConfig = CSON.readFileSync(@configFilePath) - @settings = {} # Reset to the defaults - if isPlainObject(userConfig) - @setRecursive(null, userConfig) - else - @emitter.emit 'did-change' + @setAll(userConfig) @configFileHasErrors = false catch error @configFileHasErrors = true @@ -566,16 +562,16 @@ class Config oldValue = _.clone(@get(keyPath)) _.setValueForKeyPath(@settings, keyPath, value) newValue = @get(keyPath) - @emitter.emit 'did-change', {oldValue, newValue, keyPath} if newValue isnt oldValue + @emitter.emit 'did-change', {oldValue, newValue, keyPath} unless _.isEqual(newValue, oldValue) setRawDefault: (keyPath, value) -> oldValue = _.clone(@get(keyPath)) _.setValueForKeyPath(@defaultSettings, keyPath, value) newValue = @get(keyPath) - @emitter.emit 'did-change', {oldValue, newValue, keyPath} if newValue isnt oldValue + @emitter.emit 'did-change', {oldValue, newValue, keyPath} unless _.isEqual(newValue, oldValue) setRecursive: (keyPath, value) -> - if value? and isPlainObject(value) + if isPlainObject(value) keys = if keyPath? then keyPath.split('.') else [] for key, childValue of value continue unless value.hasOwnProperty(key) @@ -587,7 +583,24 @@ class Config catch e console.warn("'#{keyPath}' could not be set. Attempted value: #{JSON.stringify(value)}; Schema: #{JSON.stringify(@getSchema(keyPath))}") - return + setAll: (newSettings) -> + unless isPlainObject(newSettings) + @settings = {} + @emitter.emit 'did-change' + return + + unsetUnspecifiedValues = (keyPath, value) => + if isPlainObject(value) + keys = if keyPath? then keyPath.split('.') else [] + for key, childValue of value + continue unless value.hasOwnProperty(key) + unsetUnspecifiedValues(keys.concat([key]).join('.'), childValue) + else + @setRawValue(keyPath, undefined) unless _.valueForKeyPath(newSettings, keyPath)? + return + + @setRecursive(null, newSettings) + unsetUnspecifiedValues(null, @settings) setDefaults: (keyPath, defaults) -> if defaults? and isPlainObject(defaults) From 33c1ce863e4e41c43935781bbc4ae9d51d93d0f1 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 1 Oct 2014 10:37:27 -0600 Subject: [PATCH 143/145] Pluralize Project API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This changes all APIs concerning paths and repositories on the project to be plural, preparing us to switch to multi-folder projects. It doesn’t make any changes to actually support multiple folders. Instead we just wrap the previous return values in singleton arrays. * constructor ‘path’ params -> ‘paths’ * getRootDirectory -> getDirectories * getPath -> getPaths * setPath -> setPaths * getRepo -> getRepositories --- spec/git-spec.coffee | 12 +++++----- spec/project-spec.coffee | 36 +++++++++++++++--------------- spec/spec-helper.coffee | 2 +- spec/window-spec.coffee | 10 ++++----- spec/workspace-spec.coffee | 2 +- spec/workspace-view-spec.coffee | 18 +++++++-------- src/atom.coffee | 4 ++-- src/project.coffee | 38 +++++++++++++++++++++++--------- src/text-editor-component.coffee | 2 +- src/text-editor.coffee | 4 ++-- src/workspace-view.coffee | 4 ++-- 11 files changed, 75 insertions(+), 57 deletions(-) diff --git a/spec/git-spec.coffee b/spec/git-spec.coffee index f29f71397..3774f14c4 100644 --- a/spec/git-spec.coffee +++ b/spec/git-spec.coffee @@ -223,7 +223,7 @@ describe "GitRepository", -> [editor] = [] beforeEach -> - atom.project.setPath(copyRepository()) + atom.project.setPaths([copyRepository()]) waitsForPromise -> atom.workspace.open('other.txt').then (o) -> editor = o @@ -232,7 +232,7 @@ describe "GitRepository", -> editor.insertNewline() statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepo().onDidChangeStatus statusHandler + atom.project.getRepositories()[0].onDidChangeStatus statusHandler editor.save() expect(statusHandler.callCount).toBe 1 expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} @@ -241,7 +241,7 @@ describe "GitRepository", -> fs.writeFileSync(editor.getPath(), 'changed') statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepo().onDidChangeStatus statusHandler + atom.project.getRepositories()[0].onDidChangeStatus statusHandler editor.getBuffer().reload() expect(statusHandler.callCount).toBe 1 expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} @@ -252,7 +252,7 @@ describe "GitRepository", -> fs.writeFileSync(editor.getPath(), 'changed') statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepo().onDidChangeStatus statusHandler + atom.project.getRepositories()[0].onDidChangeStatus statusHandler editor.getBuffer().emitter.emit 'did-change-path' expect(statusHandler.callCount).toBe 1 expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} @@ -266,7 +266,7 @@ describe "GitRepository", -> project2?.destroy() it "subscribes to all the serialized buffers in the project", -> - atom.project.setPath(copyRepository()) + atom.project.setPaths([copyRepository()]) waitsForPromise -> atom.workspace.open('file.txt') @@ -283,7 +283,7 @@ describe "GitRepository", -> buffer.append('changes') statusHandler = jasmine.createSpy('statusHandler') - project2.getRepo().onDidChangeStatus statusHandler + project2.getRepositories()[0].onDidChangeStatus statusHandler buffer.save() expect(statusHandler.callCount).toBe 1 expect(statusHandler).toHaveBeenCalledWith {path: buffer.getPath(), pathStatus: 256} diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 06e3ee29d..324772459 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -9,7 +9,7 @@ BufferedProcess = require '../src/buffered-process' describe "Project", -> beforeEach -> - atom.project.setPath(atom.project.resolve('dir')) + atom.project.setPaths([atom.project.resolve('dir')]) describe "serialization", -> deserializedProject = null @@ -41,8 +41,8 @@ describe "Project", -> describe "when an editor is saved and the project has no path", -> it "sets the project's path to the saved file's parent directory", -> tempFile = temp.openSync().path - atom.project.setPath(undefined) - expect(atom.project.getPath()).toBeUndefined() + atom.project.setPaths([]) + expect(atom.project.getPaths()[0]).toBeUndefined() editor = null waitsForPromise -> @@ -50,7 +50,7 @@ describe "Project", -> runs -> editor.saveAs(tempFile) - expect(atom.project.getPath()).toBe path.dirname(tempFile) + expect(atom.project.getPaths()[0]).toBe path.dirname(tempFile) describe ".open(path)", -> [absolutePath, newBufferHandler] = [] @@ -164,7 +164,7 @@ describe "Project", -> describe "when the project has no path", -> it "returns undefined for relative URIs", -> - atom.project.setPath() + atom.project.setPaths([]) expect(atom.project.resolve('test.txt')).toBeUndefined() expect(atom.project.resolve('http://github.com')).toBe 'http://github.com' absolutePath = fs.absolute(__dirname) @@ -173,33 +173,33 @@ describe "Project", -> describe ".setPath(path)", -> describe "when path is a file", -> it "sets its path to the files parent directory and updates the root directory", -> - atom.project.setPath(require.resolve('./fixtures/dir/a')) - expect(atom.project.getPath()).toEqual path.dirname(require.resolve('./fixtures/dir/a')) + atom.project.setPaths([require.resolve('./fixtures/dir/a')]) + expect(atom.project.getPaths()[0]).toEqual path.dirname(require.resolve('./fixtures/dir/a')) expect(atom.project.getRootDirectory().path).toEqual path.dirname(require.resolve('./fixtures/dir/a')) describe "when path is a directory", -> it "sets its path to the directory and updates the root directory", -> directory = fs.absolute(path.join(__dirname, 'fixtures', 'dir', 'a-dir')) - atom.project.setPath(directory) - expect(atom.project.getPath()).toEqual directory + atom.project.setPaths([directory]) + expect(atom.project.getPaths()[0]).toEqual directory expect(atom.project.getRootDirectory().path).toEqual directory describe "when path is null", -> it "sets its path and root directory to null", -> - atom.project.setPath(null) - expect(atom.project.getPath()?).toBeFalsy() + atom.project.setPaths([]) + expect(atom.project.getPaths()[0]?).toBeFalsy() expect(atom.project.getRootDirectory()?).toBeFalsy() it "normalizes the path to remove consecutive slashes, ., and .. segments", -> - atom.project.setPath("#{require.resolve('./fixtures/dir/a')}#{path.sep}b#{path.sep}#{path.sep}..") - expect(atom.project.getPath()).toEqual path.dirname(require.resolve('./fixtures/dir/a')) + atom.project.setPaths(["#{require.resolve('./fixtures/dir/a')}#{path.sep}b#{path.sep}#{path.sep}.."]) + expect(atom.project.getPaths()[0]).toEqual path.dirname(require.resolve('./fixtures/dir/a')) expect(atom.project.getRootDirectory().path).toEqual path.dirname(require.resolve('./fixtures/dir/a')) describe ".replace()", -> [filePath, commentFilePath, sampleContent, sampleCommentContent] = [] beforeEach -> - atom.project.setPath(atom.project.resolve('../')) + atom.project.setPaths([atom.project.resolve('../')]) filePath = atom.project.resolve('sample.js') commentFilePath = atom.project.resolve('sample-with-comments.js') @@ -332,7 +332,7 @@ describe "Project", -> it "works on evil filenames", -> platform.generateEvilFiles() - atom.project.setPath(path.join(__dirname, 'fixtures', 'evil-files')) + atom.project.setPaths([path.join(__dirname, 'fixtures', 'evil-files')]) paths = [] matches = [] waitsForPromise -> @@ -387,7 +387,7 @@ describe "Project", -> fs.removeSync(projectPath) if fs.existsSync(projectPath) it "excludes ignored files", -> - atom.project.setPath(projectPath) + atom.project.setPaths([projectPath]) atom.config.set('core.excludeVcsIgnoredPaths', true) resultHandler = jasmine.createSpy("result found") waitsForPromise -> @@ -399,7 +399,7 @@ describe "Project", -> it "includes only files when a directory filter is specified", -> projectPath = path.join(path.join(__dirname, 'fixtures', 'dir')) - atom.project.setPath(projectPath) + atom.project.setPaths([projectPath]) filePath = path.join(projectPath, 'a-dir', 'oh-git') @@ -419,7 +419,7 @@ describe "Project", -> projectPath = temp.mkdirSync() filePath = path.join(projectPath, '.text') fs.writeFileSync(filePath, 'match this') - atom.project.setPath(projectPath) + atom.project.setPaths([projectPath]) paths = [] matches = [] waitsForPromise -> diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index cc5f05c1f..199959f19 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -62,7 +62,7 @@ beforeEach -> Grim.clearDeprecations() if isCoreSpec $.fx.off = true projectPath = specProjectPath ? path.join(@specDirectory, 'fixtures') - atom.project = new Project(path: projectPath) + atom.project = new Project(paths: [projectPath]) atom.workspace = new Workspace() atom.keymaps.keyBindings = _.clone(keyBindingsToRestore) atom.commands.setRootNode(document.body) diff --git a/spec/window-spec.coffee b/spec/window-spec.coffee index 5f12a51fe..0a60c302f 100644 --- a/spec/window-spec.coffee +++ b/spec/window-spec.coffee @@ -8,7 +8,7 @@ describe "Window", -> beforeEach -> spyOn(atom, 'hide') - initialPath = atom.project.getPath() + initialPath = atom.project.getPaths()[0] spyOn(atom, 'getLoadSettings').andCallFake -> loadSettings = atom.getLoadSettings.originalValue.call(atom) loadSettings.initialPath = initialPath @@ -16,7 +16,7 @@ describe "Window", -> atom.project.destroy() windowEventHandler = new WindowEventHandler() atom.deserializeEditorWindow() - projectPath = atom.project.getPath() + projectPath = atom.project.getPaths()[0] afterEach -> windowEventHandler.unsubscribe() @@ -263,19 +263,19 @@ describe "Window", -> describe "when the project does not have a path", -> beforeEach -> - atom.project.setPath() + atom.project.setPaths([]) describe "when the opened path exists", -> it "sets the project path to the opened path", -> $(window).trigger('window:open-path', [{pathToOpen: __filename}]) - expect(atom.project.getPath()).toBe __dirname + expect(atom.project.getPaths()[0]).toBe __dirname describe "when the opened path does not exist but its parent directory does", -> it "sets the project path to the opened path's parent directory", -> $(window).trigger('window:open-path', [{pathToOpen: path.join(__dirname, 'this-path-does-not-exist.txt')}]) - expect(atom.project.getPath()).toBe __dirname + expect(atom.project.getPaths()[0]).toBe __dirname describe "when the opened path is a file", -> it "opens it in the workspace", -> diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index a175245a6..9e4ea2b7c 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -5,7 +5,7 @@ describe "Workspace", -> workspace = null beforeEach -> - atom.project.setPath(atom.project.resolve('dir')) + atom.project.setPaths([atom.project.resolve('dir')]) atom.workspace = workspace = new Workspace describe "::open(uri, options)", -> diff --git a/spec/workspace-view-spec.coffee b/spec/workspace-view-spec.coffee index f47b75e85..f2abe04f5 100644 --- a/spec/workspace-view-spec.coffee +++ b/spec/workspace-view-spec.coffee @@ -10,7 +10,7 @@ describe "WorkspaceView", -> pathToOpen = null beforeEach -> - atom.project.setPath(atom.project.resolve('dir')) + atom.project.setPaths([atom.project.resolve('dir')]) pathToOpen = atom.project.resolve('a') atom.workspace = new Workspace atom.workspaceView = atom.workspace.getView(atom.workspace).__spacePenView @@ -49,7 +49,7 @@ describe "WorkspaceView", -> expect(atom.workspaceView.getEditorViews().length).toBe 2 expect(atom.workspaceView.getActivePaneView()).toBe atom.workspaceView.getPaneViews()[1] - expect(atom.workspaceView.title).toBe "untitled - #{atom.project.getPath()}" + expect(atom.workspaceView.title).toBe "untitled - #{atom.project.getPaths()[0]}" describe "when there are open editors", -> it "constructs the view with the same panes", -> @@ -106,7 +106,7 @@ describe "WorkspaceView", -> expect(editorView3).not.toHaveFocus() expect(editorView4).not.toHaveFocus() - expect(atom.workspaceView.title).toBe "#{path.basename(editorView2.getEditor().getPath())} - #{atom.project.getPath()}" + expect(atom.workspaceView.title).toBe "#{path.basename(editorView2.getEditor().getPath())} - #{atom.project.getPaths()[0]}" describe "where there are no open editors", -> it "constructs the view with no open editors", -> @@ -144,7 +144,7 @@ describe "WorkspaceView", -> describe "window title", -> describe "when the project has no path", -> it "sets the title to 'untitled'", -> - atom.project.setPath(undefined) + atom.project.setPaths([]) expect(atom.workspaceView.title).toBe 'untitled' describe "when the project has a path", -> @@ -155,25 +155,25 @@ describe "WorkspaceView", -> describe "when there is an active pane item", -> it "sets the title to the pane item's title plus the project path", -> item = atom.workspace.getActivePaneItem() - expect(atom.workspaceView.title).toBe "#{item.getTitle()} - #{atom.project.getPath()}" + expect(atom.workspaceView.title).toBe "#{item.getTitle()} - #{atom.project.getPaths()[0]}" describe "when the title of the active pane item changes", -> it "updates the window title based on the item's new title", -> editor = atom.workspace.getActivePaneItem() editor.buffer.setPath(path.join(temp.dir, 'hi')) - expect(atom.workspaceView.title).toBe "#{editor.getTitle()} - #{atom.project.getPath()}" + expect(atom.workspaceView.title).toBe "#{editor.getTitle()} - #{atom.project.getPaths()[0]}" describe "when the active pane's item changes", -> it "updates the title to the new item's title plus the project path", -> atom.workspaceView.getActivePaneView().activateNextItem() item = atom.workspace.getActivePaneItem() - expect(atom.workspaceView.title).toBe "#{item.getTitle()} - #{atom.project.getPath()}" + expect(atom.workspaceView.title).toBe "#{item.getTitle()} - #{atom.project.getPaths()[0]}" describe "when the last pane item is removed", -> it "updates the title to contain the project's path", -> atom.workspaceView.getActivePaneView().remove() expect(atom.workspace.getActivePaneItem()).toBeUndefined() - expect(atom.workspaceView.title).toBe atom.project.getPath() + expect(atom.workspaceView.title).toBe atom.project.getPaths()[0] describe "when an inactive pane's item changes", -> it "does not update the title", -> @@ -188,7 +188,7 @@ describe "WorkspaceView", -> workspace2 = atom.workspace.testSerialization() workspaceView2 = workspace2.getView(workspace2).__spacePenView item = atom.workspace.getActivePaneItem() - expect(workspaceView2.title).toBe "#{item.getTitle()} - #{atom.project.getPath()}" + expect(workspaceView2.title).toBe "#{item.getTitle()} - #{atom.project.getPaths()[0]}" workspaceView2.remove() describe "window:toggle-invisibles event", -> diff --git a/src/atom.coffee b/src/atom.coffee index 688bec833..1dc2fedea 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -577,7 +577,7 @@ class Atom extends Model Project = require './project' startTime = Date.now() - @project ?= @deserializers.deserialize(@state.project) ? new Project(path: @getLoadSettings().initialPath) + @project ?= @deserializers.deserialize(@state.project) ? new Project(paths: [@getLoadSettings().initialPath]) @deserializeTimings.project = Date.now() - startTime deserializeWorkspaceView: -> @@ -619,7 +619,7 @@ class Atom extends Model # Notify the browser project of the window's current project path watchProjectPath: -> onProjectPathChanged = => - ipc.send('window-command', 'project-path-changed', @project.getPath()) + ipc.send('window-command', 'project-path-changed', @project.getPaths()[0]) @subscribe @project, 'path-changed', onProjectPathChanged onProjectPathChanged() diff --git a/src/project.coffee b/src/project.coffee index e57969436..9f0f3dbfe 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -10,6 +10,7 @@ Q = require 'q' Serializable = require 'serializable' TextBuffer = require 'text-buffer' {Directory} = require 'pathwatcher' +Grim = require 'grim' TextEditor = require './text-editor' Task = require './task' @@ -33,14 +34,16 @@ class Project extends Model Section: Construction and Destruction ### - constructor: ({path, @buffers}={}) -> + constructor: ({path, paths, @buffers}={}) -> @buffers ?= [] for buffer in @buffers do (buffer) => buffer.onDidDestroy => @removeBuffer(buffer) - @setPath(path) + Grim.deprecate("Pass 'paths' array instead of 'path' to project constructor") if path? + paths ?= _.compact([path]) + @setPaths(paths) destroyed: -> buffer.destroy() for buffer in @getBuffers() @@ -70,21 +73,30 @@ class Project extends Model Section: Accessing the git repository ### - # Public: Returns the {GitRepository} if available. - getRepo: -> @repo + # Public: Get an {Array} of {GitRepository}s associated with the project's + # directories. + getRepositories: -> _.compact([@repo]) + getRepo: -> + Grim.deprecate("Use ::getRepositories instead") + @repo ### Section: Managing Paths ### - # Public: Returns the project's {String} fullpath. + + # Public: Get an {Array} of {String}s containing the paths of the project's + # directories. + getPaths: -> _.compact([@rootDirectory?.path]) getPath: -> + Grim.deprecate("Use ::getPaths instead") @rootDirectory?.path - # Public: Sets the project's fullpath. + # Public: Set the paths of the project's directories. # - # * `projectPath` {String} path - setPath: (projectPath) -> + # * `projectPaths` {Array} of {String} paths. + setPaths: (projectPaths) -> + [projectPath] = projectPaths projectPath = path.normalize(projectPath) if projectPath @path = projectPath @rootDirectory?.off() @@ -100,9 +112,15 @@ class Project extends Model @rootDirectory = null @emit "path-changed" + setPath: (path) -> + Grim.deprecate("Use ::setPaths instead") + @setPaths([path]) - # Public: Returns the root {Directory} object for this project. + # Public: Get an {Array} of {Directory}s associated with this project. + getDirectories: -> + [@rootDirectory] getRootDirectory: -> + Grim.deprecate("Use ::getDirectories instead") @rootDirectory # Public: Given a uri, this resolves it relative to the project directory. If @@ -120,7 +138,7 @@ class Project extends Model else if fs.isAbsolute(uri) path.normalize(fs.absolute(uri)) - else if projectPath = @getPath() + else if projectPath = @getPaths()[0] path.normalize(fs.absolute(path.join(projectPath, uri))) else undefined diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 8519ac21d..5b2dd61d4 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -500,7 +500,7 @@ TextEditorComponent = React.createClass 'editor:fold-at-indent-level-9': -> editor.foldAllAtIndentLevel(8) 'editor:toggle-line-comments': -> editor.toggleLineCommentsInSelection() 'editor:log-cursor-scope': -> editor.logCursorScope() - 'editor:checkout-head-revision': -> atom.project.getRepo()?.checkoutHeadForEditor(editor) + 'editor:checkout-head-revision': -> atom.project.getRepositories()[0]()?.checkoutHeadForEditor(editor) 'editor:copy-path': -> editor.copyPathToClipboard() 'editor:move-line-up': -> editor.moveLineUp() 'editor:move-line-down': -> editor.moveLineDown() diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 1e3eba11b..2bd07102a 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -133,8 +133,8 @@ class TextEditor extends Model subscribeToBuffer: -> @buffer.retain() @subscribe @buffer.onDidChangePath => - unless atom.project.getPath()? - atom.project.setPath(path.dirname(@getPath())) + unless atom.project.getPaths()[0]? + atom.project.setPaths([path.dirname(@getPath())]) @emit "title-changed" @emitter.emit 'did-change-title', @getTitle() @emit "path-changed" diff --git a/src/workspace-view.coffee b/src/workspace-view.coffee index 255ec3b5b..52898a0d4 100644 --- a/src/workspace-view.coffee +++ b/src/workspace-view.coffee @@ -136,7 +136,7 @@ class WorkspaceView extends View if process.platform is 'darwin' @command 'window:install-shell-commands', => @installShellCommands() - @command 'window:run-package-specs', -> ipc.send('run-package-specs', path.join(atom.project.getPath(), 'spec')) + @command 'window:run-package-specs', -> ipc.send('run-package-specs', path.join(atom.project.getPaths()[0], 'spec')) @command 'window:focus-next-pane', => @focusNextPaneView() @command 'window:focus-previous-pane', => @focusPreviousPaneView() @@ -367,7 +367,7 @@ class WorkspaceView extends View # Updates the application's title and proxy icon based on whichever file is # open. updateTitle: -> - if projectPath = atom.project.getPath() + if projectPath = atom.project.getPaths()[0] if item = @getModel().getActivePaneItem() title = "#{item.getTitle?() ? 'untitled'} - #{projectPath}" @setTitle(title, item.getPath?()) From 99b8e159bdcbef5cab0a8d9ddcaed1986a4307fc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 1 Oct 2014 10:46:30 -0600 Subject: [PATCH 144/145] Add Project::onDidChangePaths event --- src/atom.coffee | 2 +- src/project.coffee | 18 +++++++++++++++++- src/workspace-view.coffee | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/atom.coffee b/src/atom.coffee index 1dc2fedea..a18cafe4d 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -620,7 +620,7 @@ class Atom extends Model watchProjectPath: -> onProjectPathChanged = => ipc.send('window-command', 'project-path-changed', @project.getPaths()[0]) - @subscribe @project, 'path-changed', onProjectPathChanged + @subscribe @project.onDidChangePaths(onProjectPathChanged) onProjectPathChanged() exit: (status) -> diff --git a/src/project.coffee b/src/project.coffee index 9f0f3dbfe..7493f684a 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -6,7 +6,8 @@ fs = require 'fs-plus' Q = require 'q' {deprecate} = require 'grim' {Model} = require 'theorist' -{Emitter, Subscriber} = require 'emissary' +{Subscriber} = require 'emissary' +{Emitter} = require 'event-kit' Serializable = require 'serializable' TextBuffer = require 'text-buffer' {Directory} = require 'pathwatcher' @@ -35,6 +36,7 @@ class Project extends Model ### constructor: ({path, paths, @buffers}={}) -> + @emitter = new Emitter @buffers ?= [] for buffer in @buffers @@ -69,6 +71,19 @@ class Project extends Model params.buffers = params.buffers.map (bufferState) -> atom.deserializers.deserialize(bufferState) params + + ### + Section: Event Subscription + ### + + onDidChangePaths: (callback) -> + @emitter.on 'did-change-paths', callback + + on: (eventName) -> + if eventName is 'path-changed' + Grim.deprecate("Use Project::onDidChangePaths instead") + super + ### Section: Accessing the git repository ### @@ -112,6 +127,7 @@ class Project extends Model @rootDirectory = null @emit "path-changed" + @emitter.emit 'did-change-paths', projectPaths setPath: (path) -> Grim.deprecate("Use ::setPaths instead") @setPaths([path]) diff --git a/src/workspace-view.coffee b/src/workspace-view.coffee index 52898a0d4..b4a395bca 100644 --- a/src/workspace-view.coffee +++ b/src/workspace-view.coffee @@ -102,7 +102,7 @@ class WorkspaceView extends View @subscribe $(window), 'focus', (e) => @handleFocus(e) if document.activeElement is document.body - atom.project.on 'path-changed', => @updateTitle() + atom.project.onDidChangePaths => @updateTitle() @on 'pane-container:active-pane-item-changed', => @updateTitle() @on 'pane:active-item-title-changed', '.active.pane', => @updateTitle() @on 'pane:active-item-modified-status-changed', '.active.pane', => @updateDocumentEdited() From 05ccf8adc3063ff2dccbd284c2393f23ce934100 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 1 Oct 2014 10:11:01 -0700 Subject: [PATCH 145/145] Prepare 0.135 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 505885638..67702e0e3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "0.134.0", + "version": "0.135.0", "description": "A hackable text editor for the 21st Century.", "main": "./src/browser/main.js", "repository": {