Merge pull request #3697 from atom/bo-config-scoped-properties

Add scoped settings to config
This commit is contained in:
Ben Ogle
2014-10-03 14:01:03 -07:00
8 changed files with 432 additions and 145 deletions

View File

@@ -862,3 +862,106 @@ 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.addScopedSettings(".source.coffee .string.quoted.double.coffee", foo: bar: baz: 42)
atom.config.addScopedSettings(".source .string.quoted.double", foo: bar: baz: 22)
atom.config.addScopedSettings(".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.addScopedSettings(".source.coffee .string.quoted.single", foo: bar: baz: 42)
atom.config.addScopedSettings(".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', ->
it 'falls back to the global when there is no scoped property specified', ->
atom.config.setDefaults("foo", hasDefault: 'ok')
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.addScopedSettings(".source.coffee .string.quoted.double.coffee", foo: bar: baz: 42)
atom.config.addScopedSettings(".source .string.quoted.double", foo: bar: baz: 22)
atom.config.addScopedSettings(".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", ->
disposable1 = atom.config.addScopedSettings("a", ".source.coffee .string.quoted.double.coffee", foo: bar: baz: 42)
disposable2 = atom.config.addScopedSettings("b", ".source .string.quoted.double", foo: bar: baz: 22)
disposable2.dispose()
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
describe ".observe(scopeDescriptor, keyPath)", ->
it 'calls the supplied callback when the value at the descriptor/keypath changes', ->
atom.config.observe [".source.coffee", ".string.quoted.double.coffee"], "foo.bar.baz", changeSpy = jasmine.createSpy()
expect(changeSpy).toHaveBeenCalledWith(undefined)
changeSpy.reset()
atom.config.set("foo.bar.baz", 12)
expect(changeSpy).toHaveBeenCalledWith(12)
changeSpy.reset()
disposable1 = atom.config.addScopedSettings(".source .string.quoted.double", foo: bar: baz: 22)
expect(changeSpy).toHaveBeenCalledWith(22)
changeSpy.reset()
disposable2 = atom.config.addScopedSettings("a", ".source.coffee .string.quoted.double.coffee", foo: bar: baz: 42)
expect(changeSpy).toHaveBeenCalledWith(42)
changeSpy.reset()
disposable2.dispose()
expect(changeSpy).toHaveBeenCalledWith(22)
changeSpy.reset()
disposable1.dispose()
expect(changeSpy).toHaveBeenCalledWith(12)
changeSpy.reset()
atom.config.set("foo.bar.baz", undefined)
expect(changeSpy).toHaveBeenCalledWith(undefined)
changeSpy.reset()
describe ".onDidChange(scopeDescriptor, keyPath)", ->
it 'calls the supplied callback when the value at the descriptor/keypath changes', ->
keyPath = "foo.bar.baz"
atom.config.onDidChange [".source.coffee", ".string.quoted.double.coffee"], keyPath, changeSpy = jasmine.createSpy()
atom.config.set("foo.bar.baz", 12)
expect(changeSpy).toHaveBeenCalledWith({oldValue: undefined, newValue: 12, keyPath})
changeSpy.reset()
disposable1 = atom.config.addScopedSettings(".source .string.quoted.double", foo: bar: baz: 22)
expect(changeSpy).toHaveBeenCalledWith({oldValue: 12, newValue: 22, keyPath})
changeSpy.reset()
disposable2 = atom.config.addScopedSettings("a", ".source.coffee .string.quoted.double.coffee", foo: bar: baz: 42)
expect(changeSpy).toHaveBeenCalledWith({oldValue: 22, newValue: 42, keyPath})
changeSpy.reset()
disposable2.dispose()
expect(changeSpy).toHaveBeenCalledWith({oldValue: 42, newValue: 22, keyPath})
changeSpy.reset()
disposable1.dispose()
expect(changeSpy).toHaveBeenCalledWith({oldValue: 22, newValue: 12, keyPath})
changeSpy.reset()
atom.config.set("foo.bar.baz", undefined)
expect(changeSpy).toHaveBeenCalledWith({oldValue: 12, newValue: undefined, keyPath})
changeSpy.reset()

View File

@@ -313,7 +313,7 @@ describe "PackageManager", ->
atom.packages.activatePackage("package-with-scoped-properties")
runs ->
expect(atom.syntax.getProperty ['.source.omg'], 'editor.increaseIndentPattern').toBe '^a'
expect(atom.config.get ['.source.omg'], 'editor.increaseIndentPattern').toBe '^a'
describe "converted textmate packages", ->
it "loads the package's grammars", ->
@@ -326,15 +326,18 @@ describe "PackageManager", ->
expect(atom.syntax.selectGrammar("file.rb").name).toBe "Ruby"
it "loads the translated scoped properties", ->
expect(atom.syntax.getProperty(['.source.ruby'], 'editor.commentStart')).toBeUndefined()
expect(atom.config.get(['.source.ruby'], 'editor.commentStart')).toBeUndefined()
waitsForPromise ->
atom.packages.activatePackage('language-ruby')
runs ->
expect(atom.syntax.getProperty(['.source.ruby'], 'editor.commentStart')).toBe '# '
expect(atom.config.get(['.source.ruby'], 'editor.commentStart')).toBe '# '
describe "::deactivatePackage(id)", ->
afterEach ->
atom.packages.unloadPackages()
describe "atom packages", ->
it "calls `deactivate` on the package's main module if activate was successful", ->
pack = null
@@ -436,9 +439,9 @@ describe "PackageManager", ->
atom.packages.activatePackage("package-with-scoped-properties")
runs ->
expect(atom.syntax.getProperty ['.source.omg'], 'editor.increaseIndentPattern').toBe '^a'
expect(atom.config.get ['.source.omg'], 'editor.increaseIndentPattern').toBe '^a'
atom.packages.deactivatePackage("package-with-scoped-properties")
expect(atom.syntax.getProperty ['.source.omg'], 'editor.increaseIndentPattern').toBeUndefined()
expect(atom.config.get ['.source.omg'], 'editor.increaseIndentPattern').toBeUndefined()
describe "textmate packages", ->
it "removes the package's grammars", ->
@@ -458,7 +461,7 @@ describe "PackageManager", ->
runs ->
atom.packages.deactivatePackage('language-ruby')
expect(atom.syntax.getProperty(['.source.ruby'], 'editor.commentStart')).toBeUndefined()
expect(atom.config.get(['.source.ruby'], 'editor.commentStart')).toBeUndefined()
describe "::activate()", ->
packageActivator = null

View File

@@ -82,7 +82,6 @@ beforeEach ->
spyOn(atom, 'saveSync')
atom.syntax.clearGrammarOverrides()
atom.syntax.clearProperties()
spy = spyOn(atom.packages, 'resolvePackagePath').andCallFake (packageName) ->
if specPackageName and packageName is specPackageName

View File

@@ -1,7 +1,8 @@
path = require 'path'
fs = require 'fs-plus'
# This is loaded by atom.coffee
# This is loaded by atom.coffee. See https://atom.io/docs/api/latest/Config for
# more information about config schemas.
module.exports =
core:
type: 'object'
@@ -38,6 +39,19 @@ module.exports =
editor:
type: 'object'
properties:
# These settings are used in scoped fashion only. No defaults.
commentStart:
type: ['string', 'null']
commentEnd:
type: ['string', 'null']
increaseIndentPattern:
type: ['string', 'null']
decreaseIndentPattern:
type: ['string', 'null']
foldEndPattern:
type: ['string', 'null']
# These can be used as globals or scoped, thus defaults.
fontFamily:
type: 'string'
default: ''

View File

@@ -1,13 +1,15 @@
_ = require 'underscore-plus'
fs = require 'fs-plus'
EmitterMixin = require('emissary').Emitter
{Emitter} = require 'event-kit'
{Disposable, Emitter} = require 'event-kit'
CSON = require 'season'
path = require 'path'
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.
@@ -219,7 +221,7 @@ pathWatcher = require 'pathwatcher'
#
# 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.
# of your specified type. Schema:
#
# ```coffee
# config:
@@ -229,6 +231,8 @@ pathWatcher = require 'pathwatcher'
# enum: [2, 4, 6, 8]
# ```
#
# Usage:
#
# ```coffee
# atom.config.set('my-package.someSetting', '2')
# atom.config.get('my-package.someSetting') # -> 2
@@ -307,6 +311,7 @@ class Config
properties: {}
@defaultSettings = {}
@settings = {}
@scopedSettingsStore = new ScopedPropertyStore
@configFileHasErrors = false
@configFilePath = fs.resolve(@configDirPath, 'config', ['json', 'cson'])
@configFilePath ?= path.join(@configDirPath, 'config.cson')
@@ -319,29 +324,57 @@ class Config
# than {::onDidChange} in that it will immediately call your callback with the
# current value of the config entry.
#
# ### Examples
#
# You might want to be notified when the themes change. We'll watch
# `core.themes` for changes
#
# ```coffee
# atom.config.observe 'core.themes', (value) ->
# # do stuff with value
# ```
#
# * `scopeDescriptor` (optional) {Array} of {String}s describing a path from
# the root of the syntax tree to a token. Get one by calling
# {TextEditor::scopesAtCursor}. See {::get} for examples.
# * `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
#
# 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
observe: (scopeDescriptor, keyPath, options, callback) ->
args = Array::slice.call(arguments)
if args.length is 2
# observe(keyPath, callback)
[keyPath, callback, scopeDescriptor, options] = args
else if args.length is 3 and Array.isArray(scopeDescriptor)
# observe(scopeDescriptor, keyPath, callback)
[scopeDescriptor, keyPath, callback, options] = args
else if args.length is 3 and _.isString(scopeDescriptor) and _.isObject(keyPath)
# observe(keyPath, options, callback) # Deprecated!
[keyPath, options, callback, scopeDescriptor] = args
message = ""
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}"
deprecate "Config::observe no longer supports options; see https://atom.io/docs/api/latest/Config. #{message}"
else
console.error 'An unsupported form of Config::observe is being used. See https://atom.io/docs/api/latest/Config for details'
return
callback(_.clone(@get(keyPath))) unless options.callNow == false
@emitter.on 'did-change', (event) ->
callback(event.newValue) if keyPath? and keyPath.indexOf(event?.keyPath) is 0
if scopeDescriptor?
@observeScopedKeyPath(scopeDescriptor, keyPath, callback)
else
@observeKeyPath(keyPath, options ? {}, callback)
# 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` (optional) {String} name of the key to observe
# * `scopeDescriptor` (optional) {Array} of {String}s describing a path from
# the root of the syntax tree to a token. Get one by calling
# {TextEditor::scopesAtCursor}. See {::get} for examples.
# * `keyPath` (optional) {String} name of the key to observe. Must be
# specified if `scopeDescriptor` is specified.
# * `callback` {Function} to call when the value of the key changes.
# * `event` {Object}
# * `newValue` the new value of the key
@@ -350,13 +383,17 @@ class Config
#
# Returns a {Disposable} with the following keys on which you can call
# `.dispose()` to unsubscribe.
onDidChange: (keyPath, callback) ->
onDidChange: (scopeDescriptor, keyPath, callback) ->
args = Array::slice.call(arguments)
if arguments.length is 1
callback = keyPath
keyPath = undefined
[callback, scopeDescriptor, keyPath] = args
else if arguments.length is 2
[keyPath, callback, scopeDescriptor] = args
@emitter.on 'did-change', (event) ->
callback(event) if not keyPath? or (keyPath? and keyPath.indexOf(event?.keyPath) is 0)
if scopeDescriptor?
@onDidChangeScopedKeyPath(scopeDescriptor, keyPath, callback)
else
@onDidChangeKeyPath(keyPath, callback)
###
Section: Managing Settings
@@ -364,29 +401,84 @@ class Config
# Essential: Retrieves the setting for the given key.
#
# ### Examples
#
# You might want to know what themes are enabled, so check `core.themes`
#
# ```coffee
# atom.config.get('core.themes')
# ```
#
# With scope descriptors you can get settings within a specific editor
# scope. For example, you might want to know `editor.tabLength` for ruby
# files.
#
# ```coffee
# atom.config.get(['source.ruby'], 'editor.tabLength') # => 2
# ```
#
# This setting in ruby files might be different than the global tabLength setting
#
# ```coffee
# atom.config.get('editor.tabLength') # => 4
# atom.config.get(['source.ruby'], 'editor.tabLength') # => 2
# ```
#
# Additionally, you can get the setting at the specific cursor position.
#
# ```coffee
# scopeDescriptor = @editor.scopesAtCursor()
# atom.config.get(scopeDescriptor, 'editor.tabLength') # => 2
# ```
#
# * `scopeDescriptor` (optional) {Array} of {String}s describing a path from
# the root of the syntax tree to a token. Get one by calling
# {TextEditor::scopesAtCursor}
# * `keyPath` The {String} name of the key to retrieve.
#
# 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.
#
# This value is stored in Atom's internal configuration file.
#
# ### Examples
#
# You might want to change the themes programmatically:
#
# ```coffee
# atom.config.set('core.themes', ['atom-light-ui', 'atom-light-syntax'])
# ```
#
# You can also set scoped settings. For example, you might want change the
# `editor.tabLength` only for ruby files.
#
# ```coffee
# atom.config.get('editor.tabLength') # => 4
# atom.config.get(['source.ruby'], 'editor.tabLength') # => 4
# atom.config.get(['source.js'], 'editor.tabLength') # => 4
#
# # Set ruby to 2
# atom.config.set('source.ruby', 'editor.tabLength', 2) # => true
#
# # Notice it's only set to 2 in the case of ruby
# atom.config.get('editor.tabLength') # => 4
# atom.config.get(['source.ruby'], 'editor.tabLength') # => 2
# atom.config.get(['source.js'], 'editor.tabLength') # => 4
# ```
#
# * `scope` (optional) {String}. eg. '.source.ruby'
# * `keyPath` The {String} name of the key.
# * `value` The value of the setting. Passing `undefined` will revert the
# setting to the default value.
@@ -394,18 +486,27 @@ 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) ->
if arguments.length < 3
value = keyPath
keyPath = scope
scope = undefined
unless value == undefined
try
value = @makeValueConformToSchema(keyPath, value)
catch e
return false
@setRawValue(keyPath, value)
if scope?
@setRawScopedValue(scope, keyPath, value)
else
@setRawValue(keyPath, value)
@save() unless @configFileHasErrors
true
# Extended: Restore the key path to its default value.
# Extended: Restore the global setting at `keyPath` to its default value.
#
# * `keyPath` The {String} name of the key.
#
@@ -414,7 +515,7 @@ class Config
@set(keyPath, _.valueForKeyPath(@defaultSettings, keyPath))
@get(keyPath)
# Extended: Get the default value of the key path. _Please note_ that in most
# Extended: Get the global 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.
#
@@ -425,7 +526,7 @@ class Config
defaultValue = _.valueForKeyPath(@defaultSettings, keyPath)
_.deepClone(defaultValue)
# Extended: Is the key path value its default value?
# Extended: Is the value at `keyPath` its default value?
#
# * `keyPath` The {String} name of the key.
#
@@ -434,7 +535,7 @@ class Config
isDefault: (keyPath) ->
not _.valueForKeyPath(@settings, keyPath)?
# Extended: Retrieve the schema for a specific key path. The shema will tell
# Extended: Retrieve the schema for a specific key path. The schema will tell
# you what type the keyPath expects, and other metadata about the config
# option.
#
@@ -450,7 +551,8 @@ class Config
schema = schema.properties[key]
schema
# Extended: Returns a new {Object} containing all of settings and defaults.
# Extended: Returns a new {Object} containing all of the global settings and
# defaults. This does not include scoped settings.
getSettings: ->
_.deepExtend(@settings, @defaultSettings)
@@ -484,7 +586,7 @@ class Config
deprecate 'Config::unobserve no longer does anything. Call `.dispose()` on the object returned by Config::observe instead.'
###
Section: Private
Section: Internal methods used by core
###
pushAtKeyPath: (keyPath, value) ->
@@ -505,6 +607,34 @@ class Config
@set(keyPath, arrayValue)
result
setSchema: (keyPath, schema) ->
unless isPlainObject(schema)
throw new Error("Error loading schema for #{keyPath}: schemas can only be objects!")
unless typeof schema.type?
throw new Error("Error loading schema for #{keyPath}: schema objects must have a type attribute")
rootSchema = @schema
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))
load: ->
@initializeConfigDirectory()
@loadUserConfig()
@observeUserConfig()
###
Section: Private methods managing the user's config file
###
initializeConfigDirectory: (done) ->
return if fs.existsSync(@configDirPath)
@@ -521,11 +651,6 @@ class Config
queue.push({sourcePath, destinationPath})
fs.traverseTree(templateConfigDirPath, onConfigDirFile, (path) -> true)
load: ->
@initializeConfigDirectory()
@loadUserConfig()
@observeUserConfig()
loadUserConfig: ->
unless fs.existsSync(@configFilePath)
fs.makeTreeSync(path.dirname(@configFilePath))
@@ -555,33 +680,9 @@ class Config
save: ->
CSON.writeFileSync(@configFilePath, @settings)
setRawValue: (keyPath, 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} 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} unless _.isEqual(newValue, oldValue)
setRecursive: (keyPath, value) ->
if isPlainObject(value)
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)
@setRawValue(keyPath, value)
catch e
console.warn("'#{keyPath}' could not be set. Attempted value: #{JSON.stringify(value)}; Schema: #{JSON.stringify(@getSchema(keyPath))}")
###
Section: Private methods managing global settings
###
setAll: (newSettings) ->
unless isPlainObject(newSettings)
@@ -602,6 +703,55 @@ class Config
@setRecursive(null, newSettings)
unsetUnspecifiedValues(null, @settings)
setRecursive: (keyPath, value) ->
if isPlainObject(value)
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)
@setRawValue(keyPath, value)
catch e
console.warn("'#{keyPath}' could not be set. Attempted value: #{JSON.stringify(value)}; Schema: #{JSON.stringify(@getSchema(keyPath))}")
getRawValue: (keyPath) ->
value = _.valueForKeyPath(@settings, keyPath)
defaultValue = _.valueForKeyPath(@defaultSettings, keyPath)
if value?
value = _.deepClone(value)
_.defaults(value, defaultValue) if isPlainObject(value) and isPlainObject(defaultValue)
else
value = _.deepClone(defaultValue)
value
setRawValue: (keyPath, 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} unless _.isEqual(newValue, oldValue)
observeKeyPath: (keyPath, options, callback) ->
callback(_.clone(@get(keyPath))) unless options.callNow == false
@emitter.on 'did-change', (event) ->
callback(event.newValue) if keyPath? and keyPath.indexOf(event?.keyPath) is 0
onDidChangeKeyPath: (keyPath, callback) ->
@emitter.on 'did-change', (event) ->
callback(event) if not keyPath? or (keyPath? and keyPath.indexOf(event?.keyPath) is 0)
setRawDefault: (keyPath, value) ->
oldValue = _.clone(@get(keyPath))
_.setValueForKeyPath(@defaultSettings, keyPath, value)
newValue = @get(keyPath)
@emitter.emit 'did-change', {oldValue, newValue, keyPath} unless _.isEqual(newValue, oldValue)
setDefaults: (keyPath, defaults) ->
if defaults? and isPlainObject(defaults)
keys = if keyPath? then keyPath.split('.') else []
@@ -615,25 +765,6 @@ class Config
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)
throw new Error("Error loading schema for #{keyPath}: schemas can only be objects!")
unless typeof schema.type?
throw new Error("Error loading schema for #{keyPath}: schema objects must have a type attribute")
rootSchema = @schema
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))
extractDefaultsFromSchema: (schema) ->
if schema.default?
schema.default
@@ -647,6 +778,71 @@ class Config
value = @constructor.executeSchemaEnforcers(keyPath, value, schema) if schema = @getSchema(keyPath)
value
###
Section: Private Scoped Settings
###
addScopedSettings: (name, selector, value) ->
if arguments.length < 3
value = selector
selector = name
name = null
settingsBySelector = {}
settingsBySelector[selector] = value
disposable = @scopedSettingsStore.addProperties(name, settingsBySelector)
@emitter.emit 'did-change'
new Disposable =>
disposable.dispose()
@emitter.emit 'did-change'
setRawScopedValue: (selector, keyPath, value) ->
if keyPath?
newValue = {}
_.setValueForKeyPath(newValue, keyPath, value)
value = newValue
@addScopedSettings(null, selector, value)
getRawScopedValue: (scopeDescriptor, keyPath) ->
scopeChain = scopeDescriptor
.map (scope) ->
scope = ".#{scope}" unless scope[0] is '.'
scope
.join(' ')
@scopedSettingsStore.getPropertyValue(scopeChain, keyPath)
observeScopedKeyPath: (scopeDescriptor, keyPath, callback) ->
oldValue = @get(scopeDescriptor, keyPath)
callback(oldValue)
didChange = =>
newValue = @get(scopeDescriptor, keyPath)
callback(newValue) unless _.isEqual(oldValue, newValue)
oldValue = newValue
@emitter.on 'did-change', didChange
onDidChangeScopedKeyPath: (scopeDescriptor, keyPath, callback) ->
oldValue = @get(scopeDescriptor, keyPath)
didChange = =>
newValue = @get(scopeDescriptor, keyPath)
callback({oldValue, newValue, keyPath}) unless _.isEqual(oldValue, newValue)
oldValue = newValue
@emitter.on 'did-change', didChange
# TODO: figure out how to change / remove this. The return value is awkward.
# * language mode uses it for one thing.
# * autocomplete uses it for editor.completions
settingsForScopeDescriptor: (scopeDescriptor, keyPath) ->
scopeChain = scopeDescriptor
.map (scope) ->
scope = ".#{scope}" unless scope[0] is '.'
scope
.join(' ')
@scopedSettingsStore.getProperties(scopeChain, keyPath)
# 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.
@@ -692,7 +888,7 @@ Config.addSchemaEnforcers
'null':
# null sort of isnt supported. It will just unset in this case
coerce: (keyPath, value, schema) ->
throw new Error("Validation failed at #{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 in [undefined, null]
value
'object':

View File

@@ -30,11 +30,11 @@ class LanguageMode
# Returns an {Array} of the commented {Ranges}.
toggleLineCommentsForBufferRows: (start, end) ->
scopes = @editor.scopesForBufferPosition([start, 0])
properties = atom.syntax.propertiesForScope(scopes, "editor.commentStart")[0]
properties = atom.config.settingsForScopeDescriptor(scopes, 'editor.commentStart')[0]
return unless properties
commentStartString = _.valueForKeyPath(properties, "editor.commentStart")
commentEndString = _.valueForKeyPath(properties, "editor.commentEnd")
commentStartString = _.valueForKeyPath(properties, 'editor.commentStart')
commentEndString = _.valueForKeyPath(properties, 'editor.commentEnd')
return unless commentStartString
@@ -312,7 +312,7 @@ class LanguageMode
@editor.setIndentationForBufferRow(bufferRow, desiredIndentLevel)
getRegexForProperty: (scopes, property) ->
if pattern = atom.syntax.getProperty(scopes, property)
if pattern = atom.config.get(scopes, property)
new OnigRegExp(pattern)
increaseIndentRegexForScopes: (scopes) ->

View File

@@ -1,4 +1,5 @@
CSON = require 'season'
{CompositeDisposable} = require 'event-kit'
module.exports =
class ScopedProperties
@@ -10,10 +11,12 @@ class ScopedProperties
callback(null, new ScopedProperties(scopedPropertiesPath, scopedProperties))
constructor: (@path, @scopedProperties) ->
@propertyDisposable = new CompositeDisposable
activate: ->
for selector, properties of @scopedProperties
atom.syntax.addProperties(@path, selector, properties)
@propertyDisposable.add atom.config.addScopedSettings(@path, selector, properties)
return
deactivate: ->
atom.syntax.removeProperties(@path)
@propertyDisposable.dispose()

View File

@@ -28,7 +28,6 @@ class Syntax extends GrammarRegistry
constructor: ->
super(maxTokensPerLine: 100)
@propertyStore = new ScopedPropertyStore
serialize: ->
{deserializer: @constructor.name, @grammarOverridesByPath}
@@ -36,52 +35,22 @@ class Syntax extends GrammarRegistry
createToken: (value, scopes) -> new Token({value, scopes})
# Deprecated: Used by settings-view to display snippets for packages
@::accessor 'scopedProperties', ->
deprecate("Use Syntax::getProperty instead")
@propertyStore.propertySets
@::accessor 'propertyStore', ->
deprecate("Do not use this. Use a public method on Config")
atom.config.scopedSettingsStore
addProperties: (args...) ->
name = args.shift() if args.length > 2
[selector, properties] = args
propertiesBySelector = {}
propertiesBySelector[selector] = properties
@propertyStore.addProperties(name, propertiesBySelector)
deprecate 'Consider using atom.config.set() instead. A direct (but private) replacement is available at atom.config.addScopedSettings().'
atom.config.addScopedSettings(args...)
removeProperties: (name) ->
@propertyStore.removeProperties(name)
deprecate 'atom.config.addScopedSettings() now returns a disposable you can call .dispose() on'
atom.config.scopedSettingsStore.removeProperties(name)
clearProperties: ->
@propertyStore = new ScopedPropertyStore
# Public: Get a property for the given scope and key path.
#
# ## Examples
#
# ```coffee
# comment = atom.syntax.getProperty(['.source.ruby'], 'editor.commentStart')
# console.log(comment) # '# '
# ```
#
# * `scope` An {Array} of {String} scopes.
# * `keyPath` A {String} key path.
#
# Returns a {String} property value or undefined.
getProperty: (scope, keyPath) ->
scopeChain = scope
.map (scope) ->
scope = ".#{scope}" unless scope[0] is '.'
scope
.join(' ')
@propertyStore.getPropertyValue(scopeChain, keyPath)
deprecate 'A direct (but private) replacement is available at atom.config.getRawScopedValue().'
atom.config.getRawScopedValue(scope, keyPath)
propertiesForScope: (scope, keyPath) ->
scopeChain = scope
.map (scope) ->
scope = ".#{scope}" unless scope[0] is '.'
scope
.join(' ')
@propertyStore.getProperties(scopeChain, keyPath)
cssSelectorFromScopeSelector: (scopeSelector) ->
new ScopeSelector(scopeSelector).toCssSelector()
deprecate 'A direct (but private) replacement is available at atom.config.scopedSettingsForScopeDescriptor().'
atom.config.settingsForScopeDescriptor(scope, keyPath)