mirror of
https://github.com/atom/atom.git
synced 2026-01-21 04:48:12 -05:00
Merge pull request #3697 from atom/bo-config-scoped-properties
Add scoped settings to config
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: ''
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user