diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 0c9251054..b7675e29f 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -862,3 +862,49 @@ describe "Config", -> expect(atom.config.set('foo.bar.arr', ['two', 'three'])).toBe true expect(atom.config.get('foo.bar.arr')).toEqual ['two', 'three'] + + describe "scoped settings", -> + describe ".get(scopeDescriptor, keyPath)", -> + it "returns the property with the most specific scope selector", -> + atom.config.addScopedDefaults(".source.coffee .string.quoted.double.coffee", foo: bar: baz: 42) + atom.config.addScopedDefaults(".source .string.quoted.double", foo: bar: baz: 22) + atom.config.addScopedDefaults(".source", foo: bar: baz: 11) + + expect(atom.config.get([".source.coffee", ".string.quoted.double.coffee"], "foo.bar.baz")).toBe 42 + expect(atom.config.get([".source.js", ".string.quoted.double.js"], "foo.bar.baz")).toBe 22 + expect(atom.config.get([".source.js", ".variable.assignment.js"], "foo.bar.baz")).toBe 11 + expect(atom.config.get([".text"], "foo.bar.baz")).toBeUndefined() + + it "favors the most recently added properties in the event of a specificity tie", -> + atom.config.addScopedDefaults(".source.coffee .string.quoted.single", foo: bar: baz: 42) + atom.config.addScopedDefaults(".source.coffee .string.quoted.double", foo: bar: baz: 22) + + expect(atom.config.get([".source.coffee", ".string.quoted.single"], "foo.bar.baz")).toBe 42 + expect(atom.config.get([".source.coffee", ".string.quoted.single.double"], "foo.bar.baz")).toBe 22 + + describe 'when there are global defaults', -> + beforeEach -> + atom.config.setDefaults("foo", hasDefault: 'ok') + + it 'falls back to the global when there is no scoped property specified', -> + expect(atom.config.get([".source.coffee", ".string.quoted.single"], "foo.hasDefault")).toBe 'ok' + + describe ".set(scope, keyPath, value)", -> + it "sets the value and overrides the others", -> + atom.config.addScopedDefaults(".source.coffee .string.quoted.double.coffee", foo: bar: baz: 42) + atom.config.addScopedDefaults(".source .string.quoted.double", foo: bar: baz: 22) + atom.config.addScopedDefaults(".source", foo: bar: baz: 11) + + expect(atom.config.get([".source.coffee", ".string.quoted.double.coffee"], "foo.bar.baz")).toBe 42 + + expect(atom.config.set(".source.coffee .string.quoted.double.coffee", "foo.bar.baz", 100)).toBe true + expect(atom.config.get([".source.coffee", ".string.quoted.double.coffee"], "foo.bar.baz")).toBe 100 + + describe ".removeScopedSettingsForName(name)", -> + it "allows properties to be removed by name", -> + atom.config.addScopedDefaults("a", ".source.coffee .string.quoted.double.coffee", foo: bar: baz: 42) + atom.config.addScopedDefaults("b", ".source .string.quoted.double", foo: bar: baz: 22) + + atom.config.removeScopedSettingsForName("b") + expect(atom.config.get([".source.js", ".string.quoted.double.js"], "foo.bar.baz")).toBeUndefined() + expect(atom.config.get([".source.coffee", ".string.quoted.double.coffee"], "foo.bar.baz")).toBe 42 diff --git a/src/config.coffee b/src/config.coffee index 6c192298e..fe2723762 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -8,6 +8,8 @@ async = require 'async' pathWatcher = require 'pathwatcher' {deprecate} = require 'grim' +ScopedPropertyStore = require 'scoped-property-store' + # Essential: Used to access all of Atom's configuration details. # # An instance of this class is always available as the `atom.config` global. @@ -307,6 +309,7 @@ class Config properties: {} @defaultSettings = {} @settings = {} + @scopedSettingsStore = new ScopedPropertyStore @configFileHasErrors = false @configFilePath = fs.resolve(@configDirPath, 'config', ['json', 'cson']) @configFilePath ?= path.join(@configDirPath, 'config.cson') @@ -368,20 +371,16 @@ class Config # # Returns the value from Atom's default settings, the user's configuration # file in the type specified by the configuration schema. - get: (keyPath) -> - value = _.valueForKeyPath(@settings, keyPath) - defaultValue = _.valueForKeyPath(@defaultSettings, keyPath) + get: (scopeDescriptor, keyPath) -> + if arguments.length == 1 + keyPath = scopeDescriptor + scopeDescriptor = undefined - if value? - value = _.deepClone(value) - valueIsObject = _.isObject(value) and not _.isArray(value) - defaultValueIsObject = _.isObject(defaultValue) and not _.isArray(defaultValue) - if valueIsObject and defaultValueIsObject - _.defaults(value, defaultValue) - else - value = _.deepClone(defaultValue) + if scopeDescriptor? + value = @getRawScopedValue(scopeDescriptor, keyPath) + return value if value? - value + @getRawValue(keyPath) # Essential: Sets the value for a configuration setting. # @@ -394,14 +393,23 @@ class Config # 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) -> + set: (scope, keyPath, value) -> unless value == undefined try value = @makeValueConformToSchema(keyPath, value) catch e return false - @setRawValue(keyPath, value) + if arguments.length < 3 + value = keyPath + keyPath = scope + scope = undefined + + if scope? + @addRawScopedValue(scope, keyPath, value) + else + @setRawValue(keyPath, value) + @save() unless @configFileHasErrors true @@ -578,6 +586,18 @@ class Config save: -> CSON.writeFileSync(@configFilePath, @settings) + getRawValue: (keyPath) -> + value = _.valueForKeyPath(@settings, keyPath) + defaultValue = _.valueForKeyPath(@defaultSettings, keyPath) + + if value? + value = _.deepClone(value) + _.defaults(value, defaultValue) if isPlainObject(defaultValue) and isPlainObject(defaultValue) + else + value = _.deepClone(defaultValue) + + value + setRawValue: (keyPath, value) -> defaultValue = _.valueForKeyPath(@defaultSettings, keyPath) value = undefined if _.isEqual(defaultValue, value) @@ -651,6 +671,43 @@ class Config value = @constructor.executeSchemaEnforcers(keyPath, value, schema) if schema = @getSchema(keyPath) value + ### + Section: Private Scoped Settings + ### + + addScopedDefaults: (name, selector, value) -> + if arguments.length < 3 + value = selector + selector = name + name = null + + name = "#{name ? ''}+default" + settingsBySelector = {} + settingsBySelector[selector] = value + @scopedSettingsStore.addProperties(name, settingsBySelector) + + addRawScopedValue: (selector, keyPath, value) -> + if keyPath? + newValue = {} + _.setValueForKeyPath(newValue, keyPath, value) + value = newValue + + settingsBySelector = {} + settingsBySelector[selector] = value + @scopedSettingsStore.addProperties(name, settingsBySelector) + + getRawScopedValue: (scopeDescriptor, keyPath) -> + scopeChain = scopeDescriptor + .map (scope) -> + scope = ".#{scope}" unless scope[0] is '.' + scope + .join(' ') + @scopedSettingsStore.getPropertyValue(scopeChain, keyPath) + + removeScopedSettingsForName: (name) -> + @scopedSettingsStore.removeProperties(name) + @scopedSettingsStore.removeProperties("#{name}+default") + # 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.