2018-01-18 22:12:57 +03:00
const _ = require ( 'underscore-plus' )
const { Emitter } = require ( 'event-kit' )
2018-01-18 03:30:30 +03:00
const {
getValueAtKeyPath , setValueAtKeyPath , deleteValueAtKeyPath ,
2018-01-18 22:12:57 +03:00
pushKeyPath , splitKeyPath
} = require ( 'key-path-helpers' )
const Color = require ( './color' )
const ScopedPropertyStore = require ( 'scoped-property-store' )
const ScopeDescriptor = require ( './scope-descriptor' )
2018-01-18 03:30:30 +03:00
2018-03-16 09:52:49 +03:00
const schemaEnforcers = { }
2018-01-18 03:30:30 +03:00
// Essential: Used to access all of Atom's configuration details.
//
// An instance of this class is always available as the `atom.config` global.
//
// ## Getting and setting config settings.
//
// ```coffee
// # Note that 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'
// ```
//
// 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 immediately and every time the value is changed
// console.log 'My configuration changed:', newValue
// ```
//
// If you want a notification only when the value changes, use {::onDidChange}.
//
// ```coffee
// atom.config.onDidChange 'my-package.myKey', ({newValue, oldValue}) ->
// console.log 'My configuration changed:', newValue, oldValue
// ```
//
// ### Value Coercion
//
// Config settings each have a type specified by way of a
2018-08-11 08:09:33 +03:00
// [schema](json-schema.org). For example we might want an integer setting that only
2018-01-18 03:30:30 +03:00
// 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
//
// # 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 settings for your package
//
// Define a schema under a `config` key in your package main.
//
// ```coffee
// module.exports =
// # Your config schema
// config:
// someInt:
// type: 'integer'
// default: 23
// minimum: 1
//
// activate: (state) -> # ...
// # ...
// ```
//
// See [package docs](http://flight-manual.atom.io/hacking-atom/sections/package-word-count/) for
// more info.
//
// ## Config Schemas
//
// We use [json schema](http://json-schema.org) which allows you to define your value's
// 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
//
// 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.
//
// ```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. `'true'` and `'false'` will be coerced into
// a boolean. Numbers, arrays, objects, and anything else will not be coerced.
//
// ```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
// ```
//
// #### color
//
// Values will be coerced into a {Color} with `red`, `green`, `blue`, and `alpha`
// properties that all have numeric values. `red`, `green`, `blue` will be in
// the range 0 to 255 and `value` will be in the range 0 to 1. Values can be any
// valid CSS color format such as `#abc`, `#abcdef`, `white`,
// `rgb(50, 100, 150)`, and `rgba(25, 75, 125, .75)`.
//
// ```coffee
// config:
// someSetting:
// type: 'color'
// default: 'white'
// ```
//
// #### object / Grouping other types
//
// A config setting with the type `object` allows grouping a set of config
// settings. The group will be visually separated and has its own group headline.
// The sub options must be listed under a `properties` key.
//
// ```coffee
// config:
// someSetting:
// type: 'object'
// properties:
// myChildIntOption:
// type: 'integer'
// minimum: 1.5
// maximum: 11.5
// ```
//
// ### Other Supported Keys
//
// #### enum
//
// All types support an `enum` key, which lets you specify all the values the
// setting can take. `enum` may be an array of allowed values (of the specified
// type), or an array of objects with `value` and `description` properties, where
// the `value` is an allowed value, and the `description` is a descriptive string
// used in the settings view.
//
// In this example, the setting must be one of the 4 integers:
//
// ```coffee
// config:
// someSetting:
// type: 'integer'
// default: 4
// enum: [2, 4, 6, 8]
// ```
//
// In this example, the setting must be either 'foo' or 'bar', which are
// presented using the provided descriptions in the settings pane:
//
// ```coffee
// config:
// someSetting:
// type: 'string'
// default: 'foo'
// enum: [
// {value: 'foo', description: 'Foo mode. You want this.'}
// {value: 'bar', description: 'Bar mode. Nobody wants that!'}
// ]
// ```
//
// Usage:
//
// ```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 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.
//
// Descriptions will be displayed below the title in the settings view.
//
// For a group of config settings the humanized key or the title and the
// description are used for the group headline.
//
// ```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 setting that
// you do not need to specify a title or description!
//
// Descriptions allow a subset of
// [Markdown formatting](https://help.github.com/articles/github-flavored-markdown/).
// Specifically, you may use the following in configuration setting descriptions:
//
// * **bold** - `**bold**`
// * *italics* - `*italics*`
// * [links](https://atom.io) - `[links](https://atom.io)`
2018-09-13 09:08:20 +03:00
// * `code spans` - `` `code spans` ``
2018-01-18 03:30:30 +03:00
// * line breaks - `line breaks<br/>`
// * ~~strikethrough~~ - `~~strikethrough~~`
//
// #### order
//
// The settings view orders your settings alphabetically. You can override this
// ordering with the order key.
//
// ```coffee
// config:
// zSetting:
// type: 'integer'
// default: 4
// order: 1
// aSetting:
// type: 'integer'
// default: 4
// order: 2
// ```
//
// ## Manipulating values outside your configuration schema
//
// It is possible to manipulate(`get`, `set`, `observe` etc) values that do not
// appear in your configuration schema. For example, if the config schema of the
// package 'some-package' is
//
// ```coffee
// config:
// someSetting:
// type: 'boolean'
// default: false
// ```
//
// You can still do the following
//
// ```coffee
// let otherSetting = atom.config.get('some-package.otherSetting')
// atom.config.set('some-package.stillAnotherSetting', otherSetting * 5)
// ```
//
// In other words, if a function asks for a `key-path`, that path doesn't have to
// be described in the config schema for the package or any package. However, as
// highlighted in the best practices section, you are advised against doing the
// above.
//
// ## Best practices
//
// * Don't depend on (or write to) configuration keys outside of your keypath.
//
2018-01-19 03:46:00 +03:00
class Config {
2018-01-18 22:12:57 +03:00
static addSchemaEnforcer ( typeName , enforcerFunction ) {
if ( schemaEnforcers [ typeName ] == null ) { schemaEnforcers [ typeName ] = [ ] }
return schemaEnforcers [ typeName ] . push ( enforcerFunction )
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
static addSchemaEnforcers ( filters ) {
for ( let typeName in filters ) {
const functions = filters [ typeName ]
for ( let name in functions ) {
const enforcerFunction = functions [ name ]
this . addSchemaEnforcer ( typeName , enforcerFunction )
2018-01-18 03:30:30 +03:00
}
}
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
static executeSchemaEnforcers ( keyPath , value , schema ) {
let error = null
let types = schema . type
if ( ! Array . isArray ( types ) ) { types = [ types ] }
2018-01-19 00:46:38 +03:00
for ( let type of types ) {
2018-01-18 22:12:57 +03:00
try {
const enforcerFunctions = schemaEnforcers [ type ] . concat ( schemaEnforcers [ '*' ] )
2018-01-19 21:18:28 +03:00
for ( let enforcer of enforcerFunctions ) {
// At some point in one's life, one must call upon an enforcer.
2018-01-18 22:12:57 +03:00
value = enforcer . call ( this , keyPath , value , schema )
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
error = null
break
} catch ( e ) {
error = e
2018-01-18 03:30:30 +03:00
}
}
2018-01-18 22:12:57 +03:00
if ( error != null ) { throw error }
return value
}
2018-01-19 03:46:00 +03:00
// Created during initialization, available as `atom.config`
2018-01-25 05:16:49 +03:00
constructor ( params = { } ) {
2018-01-18 22:12:57 +03:00
this . clear ( )
2018-01-25 05:16:49 +03:00
this . initialize ( params )
2018-01-18 22:12:57 +03:00
}
2018-01-25 05:16:49 +03:00
initialize ( { saveCallback , mainSource , projectHomeSchema } ) {
if ( saveCallback ) {
this . saveCallback = saveCallback
}
if ( mainSource ) this . mainSource = mainSource
if ( projectHomeSchema ) {
this . schema . properties . core . properties . projectHome = projectHomeSchema
this . defaultSettings . core . projectHome = projectHomeSchema . default
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
clear ( ) {
this . emitter = new Emitter ( )
this . schema = {
type : 'object' ,
properties : { }
2018-01-18 03:30:30 +03:00
}
2018-02-19 02:03:00 +03:00
2018-01-18 22:12:57 +03:00
this . defaultSettings = { }
this . settings = { }
2018-03-02 06:09:22 +03:00
this . projectSettings = { }
this . projectFile = null
2018-01-18 22:12:57 +03:00
this . scopedSettingsStore = new ScopedPropertyStore ( )
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
this . settingsLoaded = false
this . transactDepth = 0
this . pendingOperations = [ ]
2018-02-20 21:52:16 +03:00
this . legacyScopeAliases = new Map ( )
2018-01-26 22:15:35 +03:00
this . requestSave = _ . debounce ( ( ) => this . save ( ) , 1 )
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-19 03:46:00 +03:00
/ *
Section : Config Subscription
* /
// 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.
//
// ### 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
// ```
//
// * `keyPath` {String} name of the key to observe
// * `options` (optional) {Object}
// * `scope` (optional) {ScopeDescriptor} describing a path from
// the root of the syntax tree to a token. Get one by calling
// {editor.getLastCursor().getScopeDescriptor()}. See {::get} for examples.
// See [the scopes docs](http://flight-manual.atom.io/behind-atom/sections/scoped-settings-scopes-and-scope-descriptors/)
// for more information.
// * `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.
2018-01-19 21:18:28 +03:00
observe ( ... args ) {
2018-01-18 22:12:57 +03:00
let callback , keyPath , options , scopeDescriptor
2018-01-19 21:18:28 +03:00
if ( args . length === 2 ) {
[ keyPath , callback ] = args
} else if ( ( args . length === 3 ) && ( _ . isString ( args [ 0 ] ) && _ . isObject ( args [ 1 ] ) ) ) {
[ keyPath , options , callback ] = args
2018-01-18 22:12:57 +03:00
scopeDescriptor = options . scope
} else {
console . error ( 'An unsupported form of Config::observe is being used. See https://atom.io/docs/api/latest/Config for details' )
return
}
if ( scopeDescriptor != null ) {
return this . observeScopedKeyPath ( scopeDescriptor , keyPath , callback )
} else {
return this . observeKeyPath ( keyPath , options != null ? options : { } , callback )
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-19 03:46:00 +03:00
// 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. Must be
// specified if `scopeDescriptor` is specified.
// * `options` (optional) {Object}
// * `scope` (optional) {ScopeDescriptor} describing a path from
// the root of the syntax tree to a token. Get one by calling
// {editor.getLastCursor().getScopeDescriptor()}. See {::get} for examples.
// See [the scopes docs](http://flight-manual.atom.io/behind-atom/sections/scoped-settings-scopes-and-scope-descriptors/)
// for more information.
// * `callback` {Function} to call when the value of the key changes.
// * `event` {Object}
// * `newValue` the new value of the key
// * `oldValue` the prior value of the key.
//
// Returns a {Disposable} with the following keys on which you can call
// `.dispose()` to unsubscribe.
2018-01-19 21:18:28 +03:00
onDidChange ( ... args ) {
2018-01-18 22:12:57 +03:00
let callback , keyPath , scopeDescriptor
2018-01-19 21:18:28 +03:00
if ( args . length === 1 ) {
[ callback ] = args
} else if ( args . length === 2 ) {
[ keyPath , callback ] = args
2018-01-18 22:12:57 +03:00
} else {
let options ;
2018-01-19 21:18:28 +03:00
[ keyPath , options , callback ] = args
2018-01-18 22:12:57 +03:00
scopeDescriptor = options . scope
}
if ( scopeDescriptor != null ) {
return this . onDidChangeScopedKeyPath ( scopeDescriptor , keyPath , callback )
} else {
return this . onDidChangeKeyPath ( keyPath , callback )
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-19 03:46:00 +03:00
/ *
Section : Managing Settings
* /
// 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('editor.tabLength', scope: ['source.ruby']) # => 2
// ```
//
// This setting in ruby files might be different than the global tabLength setting
//
// ```coffee
// atom.config.get('editor.tabLength') # => 4
// atom.config.get('editor.tabLength', scope: ['source.ruby']) # => 2
// ```
//
// You can get the language scope descriptor via
// {TextEditor::getRootScopeDescriptor}. This will get the setting specifically
// for the editor's language.
//
// ```coffee
// atom.config.get('editor.tabLength', scope: @editor.getRootScopeDescriptor()) # => 2
// ```
//
// Additionally, you can get the setting at the specific cursor position.
//
// ```coffee
// scopeDescriptor = @editor.getLastCursor().getScopeDescriptor()
// atom.config.get('editor.tabLength', scope: scopeDescriptor) # => 2
// ```
//
// * `keyPath` The {String} name of the key to retrieve.
// * `options` (optional) {Object}
// * `sources` (optional) {Array} of {String} source names. If provided, only
// values that were associated with these sources during {::set} will be used.
// * `excludeSources` (optional) {Array} of {String} source names. If provided,
// values that were associated with these sources during {::set} will not
// be used.
// * `scope` (optional) {ScopeDescriptor} describing a path from
// the root of the syntax tree to a token. Get one by calling
// {editor.getLastCursor().getScopeDescriptor()}
// See [the scopes docs](http://flight-manual.atom.io/behind-atom/sections/scoped-settings-scopes-and-scope-descriptors/)
// for more information.
//
// Returns the value from Atom's default settings, the user's configuration
// file in the type specified by the configuration schema.
2018-01-19 21:18:28 +03:00
get ( ... args ) {
2018-03-02 06:09:22 +03:00
let keyPath , options , scope
2018-01-19 21:18:28 +03:00
if ( args . length > 1 ) {
if ( ( typeof args [ 0 ] === 'string' ) || ( args [ 0 ] == null ) ) {
2018-01-19 22:05:43 +03:00
[ keyPath , options ] = args ;
2018-01-18 22:12:57 +03:00
( { scope } = options )
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
} else {
2018-01-19 21:18:28 +03:00
[ keyPath ] = args
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
if ( scope != null ) {
2018-03-02 06:09:22 +03:00
const value = this . getRawScopedValue ( scope , keyPath , options )
2018-01-18 22:12:57 +03:00
return value != null ? value : this . getRawValue ( keyPath , options )
} else {
return this . getRawValue ( keyPath , options )
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-19 03:46:00 +03:00
// Extended: Get all of the values for the given key-path, along with their
// associated scope selector.
//
// * `keyPath` The {String} name of the key to retrieve
// * `options` (optional) {Object} see the `options` argument to {::get}
//
// Returns an {Array} of {Object}s with the following keys:
// * `scopeDescriptor` The {ScopeDescriptor} with which the value is associated
// * `value` The value for the key-path
2018-03-02 06:09:22 +03:00
getAll ( keyPath , options ) {
let globalValue , result , scope
2018-01-18 22:12:57 +03:00
if ( options != null ) { ( { scope } = options ) }
2018-03-02 06:09:22 +03:00
if ( scope != null ) {
let legacyScopeDescriptor
const scopeDescriptor = ScopeDescriptor . fromObject ( scope )
result = this . scopedSettingsStore . getAll (
scopeDescriptor . getScopeChain ( ) ,
keyPath ,
options
)
legacyScopeDescriptor = this . getLegacyScopeDescriptorForNewScopeDescriptor ( scopeDescriptor )
if ( legacyScopeDescriptor ) {
result . push ( ... Array . from ( this . scopedSettingsStore . getAll (
legacyScopeDescriptor . getScopeChain ( ) ,
keyPath ,
options
) || [ ] ) )
}
} else {
result = [ ]
2018-02-19 02:03:00 +03:00
}
2018-03-02 06:09:22 +03:00
globalValue = this . getRawValue ( keyPath , options )
if ( globalValue ) {
result . push ( { scopeSelector : '*' , value : globalValue } )
2018-02-19 02:03:00 +03:00
}
2018-03-02 06:09:22 +03:00
2018-02-19 02:03:00 +03:00
return result
}
2018-01-19 03:46:00 +03:00
// 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('editor.tabLength', scope: ['source.ruby']) # => 4
// atom.config.get('editor.tabLength', scope: ['source.js']) # => 4
//
// # Set ruby to 2
// atom.config.set('editor.tabLength', 2, scopeSelector: '.source.ruby') # => true
//
// # Notice it's only set to 2 in the case of ruby
// atom.config.get('editor.tabLength') # => 4
// atom.config.get('editor.tabLength', scope: ['source.ruby']) # => 2
// atom.config.get('editor.tabLength', scope: ['source.js']) # => 4
// ```
//
// * `keyPath` The {String} name of the key.
// * `value` The value of the setting. Passing `undefined` will revert the
// setting to the default value.
// * `options` (optional) {Object}
// * `scopeSelector` (optional) {String}. eg. '.source.ruby'
// See [the scopes docs](http://flight-manual.atom.io/behind-atom/sections/scoped-settings-scopes-and-scope-descriptors/)
// for more information.
// * `source` (optional) {String} The name of a file with which the setting
// is associated. Defaults to the user's config file.
//
// 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.
2018-01-19 21:18:28 +03:00
set ( ... args ) {
let [ keyPath , value , options = { } ] = args
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
if ( ! this . settingsLoaded ) {
2018-01-19 00:46:38 +03:00
this . pendingOperations . push ( ( ) => this . set ( keyPath , value , options ) )
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-19 03:51:40 +03:00
const scopeSelector = options . scopeSelector
let source = options . source
const shouldSave = options . save != null ? options . save : true
2018-01-18 03:30:30 +03:00
2018-03-02 06:09:22 +03:00
if ( source && ! scopeSelector && source !== this . projectFile ) {
2018-01-18 22:12:57 +03:00
throw new Error ( "::set with a 'source' and no 'sourceSelector' is not yet implemented!" )
}
2018-01-18 03:30:30 +03:00
2018-01-25 05:16:49 +03:00
if ( ! source ) source = this . mainSource
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
if ( value !== undefined ) {
try {
value = this . makeValueConformToSchema ( keyPath , value )
} catch ( e ) {
return false
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
if ( scopeSelector != null ) {
this . setRawScopedValue ( keyPath , value , source , scopeSelector )
} else {
2018-03-02 06:09:22 +03:00
this . setRawValue ( keyPath , value , { source } )
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-25 05:16:49 +03:00
if ( source === this . mainSource && shouldSave && this . settingsLoaded ) {
2018-01-18 22:12:57 +03:00
this . requestSave ( )
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
return true
}
2018-01-18 03:30:30 +03:00
2018-01-19 03:46:00 +03:00
// Essential: Restore the setting at `keyPath` to its default value.
//
// * `keyPath` The {String} name of the key.
// * `options` (optional) {Object}
// * `scopeSelector` (optional) {String}. See {::set}
// * `source` (optional) {String}. See {::set}
2018-01-18 22:12:57 +03:00
unset ( keyPath , options ) {
if ( ! this . settingsLoaded ) {
2018-01-19 00:46:38 +03:00
this . pendingOperations . push ( ( ) => this . unset ( keyPath , options ) )
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
let { scopeSelector , source } = options != null ? options : { }
2018-01-25 05:16:49 +03:00
if ( source == null ) { source = this . mainSource }
2018-01-18 22:12:57 +03:00
if ( scopeSelector != null ) {
if ( keyPath != null ) {
let settings = this . scopedSettingsStore . propertiesForSourceAndSelector ( source , scopeSelector )
if ( getValueAtKeyPath ( settings , keyPath ) != null ) {
this . scopedSettingsStore . removePropertiesForSourceAndSelector ( source , scopeSelector )
setValueAtKeyPath ( settings , keyPath , undefined )
settings = withoutEmptyObjects ( settings )
2018-01-19 22:01:32 +03:00
if ( settings != null ) {
this . set ( null , settings , { scopeSelector , source , priority : this . priorityForSource ( source ) } )
}
2018-01-25 05:16:49 +03:00
const configIsReady = ( source === this . mainSource ) && this . settingsLoaded
2018-01-19 22:01:32 +03:00
if ( configIsReady ) {
2018-01-18 22:12:57 +03:00
return this . requestSave ( )
2018-01-18 03:30:30 +03:00
}
}
} else {
2018-01-18 22:12:57 +03:00
this . scopedSettingsStore . removePropertiesForSourceAndSelector ( source , scopeSelector )
return this . emitChangeEvent ( )
}
} else {
for ( scopeSelector in this . scopedSettingsStore . propertiesForSource ( source ) ) {
this . unset ( keyPath , { scopeSelector , source } )
}
2018-01-25 05:16:49 +03:00
if ( ( keyPath != null ) && ( source === this . mainSource ) ) {
2018-01-18 22:12:57 +03:00
return this . set ( keyPath , getValueAtKeyPath ( this . defaultSettings , keyPath ) )
2018-01-18 03:30:30 +03:00
}
}
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-19 03:46:00 +03:00
// Extended: Get an {Array} of all of the `source` {String}s with which
// settings have been added via {::set}.
2018-01-18 22:12:57 +03:00
getSources ( ) {
return _ . uniq ( _ . pluck ( this . scopedSettingsStore . propertySets , 'source' ) ) . sort ( )
}
2018-01-18 03:30:30 +03:00
2018-01-19 03:46:00 +03:00
// 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.
//
// * `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, but is accessible
// from the root schema.
2018-01-18 22:12:57 +03:00
getSchema ( keyPath ) {
const keys = splitKeyPath ( keyPath )
let { schema } = this
2018-01-19 21:56:23 +03:00
for ( let key of keys ) {
2018-01-19 00:46:38 +03:00
let childSchema
2018-01-18 22:12:57 +03:00
if ( schema . type === 'object' ) {
childSchema = schema . properties != null ? schema . properties [ key ] : undefined
if ( childSchema == null ) {
if ( isPlainObject ( schema . additionalProperties ) ) {
childSchema = schema . additionalProperties
} else if ( schema . additionalProperties === false ) {
return null
} else {
return { type : 'any' }
2018-01-18 03:30:30 +03:00
}
}
2018-01-18 22:12:57 +03:00
} else {
return null
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
schema = childSchema
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
return schema
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
getUserConfigPath ( ) {
2018-01-25 05:16:49 +03:00
return this . mainSource
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-19 03:46:00 +03:00
// Extended: Suppress calls to handler functions registered with {::onDidChange}
// and {::observe} for the duration of `callback`. After `callback` executes,
// handlers will be called once if the value for their key-path has changed.
//
// * `callback` {Function} to execute while suppressing calls to handlers.
2018-01-18 22:12:57 +03:00
transact ( callback ) {
this . beginTransaction ( )
try {
return callback ( )
} finally {
this . endTransaction ( )
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-02-20 21:52:16 +03:00
getLegacyScopeDescriptorForNewScopeDescriptor ( scopeDescriptor ) {
2018-08-18 20:35:41 +03:00
return null
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-19 03:46:00 +03:00
/ *
Section : Internal methods used by core
* /
// Private: Suppress calls to handler functions registered with {::onDidChange}
// and {::observe} for the duration of the {Promise} returned by `callback`.
// After the {Promise} is either resolved or rejected, handlers will be called
// once if the value for their key-path has changed.
//
// * `callback` {Function} that returns a {Promise}, which will be executed
// while suppressing calls to handlers.
//
// Returns a {Promise} that is either resolved or rejected according to the
// `{Promise}` returned by `callback`. If `callback` throws an error, a
// rejected {Promise} will be returned instead.
2018-01-18 22:12:57 +03:00
transactAsync ( callback ) {
let endTransaction
this . beginTransaction ( )
try {
endTransaction = fn => ( ... args ) => {
this . endTransaction ( )
2018-01-19 21:56:23 +03:00
return fn ( ... args )
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
const result = callback ( )
2018-01-19 01:52:47 +03:00
return new Promise ( ( resolve , reject ) => {
2018-01-18 22:12:57 +03:00
return result . then ( endTransaction ( resolve ) ) . catch ( endTransaction ( reject ) )
} )
} catch ( error ) {
this . endTransaction ( )
return Promise . reject ( error )
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
beginTransaction ( ) {
2018-01-19 01:37:51 +03:00
this . transactDepth ++
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
endTransaction ( ) {
this . transactDepth --
2018-01-19 01:37:51 +03:00
this . emitChangeEvent ( )
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
pushAtKeyPath ( keyPath , value ) {
2018-01-19 21:56:23 +03:00
const left = this . get ( keyPath )
const arrayValue = ( left == null ? [ ] : left )
2018-01-18 22:12:57 +03:00
const result = arrayValue . push ( value )
this . set ( keyPath , arrayValue )
return result
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
unshiftAtKeyPath ( keyPath , value ) {
2018-01-19 21:56:23 +03:00
const left = this . get ( keyPath )
const arrayValue = ( left == null ? [ ] : left )
2018-01-18 22:12:57 +03:00
const result = arrayValue . unshift ( value )
this . set ( keyPath , arrayValue )
return result
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
removeAtKeyPath ( keyPath , value ) {
2018-01-19 21:56:23 +03:00
const left = this . get ( keyPath )
const arrayValue = ( left == null ? [ ] : left )
2018-01-18 22:12:57 +03:00
const result = _ . remove ( arrayValue , value )
this . set ( keyPath , arrayValue )
return result
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
setSchema ( keyPath , schema ) {
if ( ! isPlainObject ( schema ) ) {
throw new Error ( ` Error loading schema for ${ keyPath } : schemas can only be objects! ` )
}
2018-01-18 03:30:30 +03:00
2018-01-19 00:31:28 +03:00
if ( schema . type == null ) {
2018-01-18 22:12:57 +03:00
throw new Error ( ` Error loading schema for ${ keyPath } : schema objects must have a type attribute ` )
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
let rootSchema = this . schema
if ( keyPath ) {
2018-01-19 21:56:23 +03:00
for ( let key of splitKeyPath ( keyPath ) ) {
2018-01-18 22:12:57 +03:00
rootSchema . type = 'object'
if ( rootSchema . properties == null ) { rootSchema . properties = { } }
const { properties } = rootSchema
if ( properties [ key ] == null ) { properties [ key ] = { } }
rootSchema = properties [ key ]
2018-01-18 03:30:30 +03:00
}
}
2018-01-18 22:12:57 +03:00
Object . assign ( rootSchema , schema )
2018-01-24 23:32:04 +03:00
this . transact ( ( ) => {
2018-01-18 22:12:57 +03:00
this . setDefaults ( keyPath , this . extractDefaultsFromSchema ( schema ) )
this . setScopedDefaultsFromSchema ( keyPath , schema )
2018-01-24 23:32:04 +03:00
this . resetSettingsForSchemaChange ( )
2018-01-18 22:12:57 +03:00
} )
}
save ( ) {
2018-01-25 05:16:49 +03:00
if ( this . saveCallback ) {
let allSettings = { '*' : this . settings }
allSettings = Object . assign ( allSettings , this . scopedSettingsStore . propertiesForSource ( this . mainSource ) )
allSettings = sortObject ( allSettings )
this . saveCallback ( allSettings )
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-19 03:46:00 +03:00
/ *
Section : Private methods managing global settings
* /
2018-01-18 03:30:30 +03:00
2018-03-02 06:09:22 +03:00
resetUserSettings ( newSettings , options = { } ) {
2018-03-06 22:06:22 +03:00
this . _resetSettings ( newSettings , options )
}
_resetSettings ( newSettings , options = { } ) {
const source = options . source
2018-01-25 05:16:49 +03:00
newSettings = Object . assign ( { } , newSettings )
2018-01-18 22:12:57 +03:00
if ( newSettings . global != null ) {
newSettings [ '*' ] = newSettings . global
delete newSettings . global
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
if ( newSettings [ '*' ] != null ) {
const scopedSettings = newSettings
newSettings = newSettings [ '*' ]
delete scopedSettings [ '*' ]
2018-03-06 22:06:22 +03:00
this . resetScopedSettings ( scopedSettings , { source } )
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
return this . transact ( ( ) => {
2018-03-06 22:06:22 +03:00
this . _clearUnscopedSettingsForSource ( source )
2018-01-18 22:12:57 +03:00
this . settingsLoaded = true
2018-03-02 06:09:22 +03:00
for ( let key in newSettings ) {
const value = newSettings [ key ]
2018-03-06 22:06:22 +03:00
this . set ( key , value , { save : false , source } )
2018-03-02 06:09:22 +03:00
}
2018-01-18 22:12:57 +03:00
if ( this . pendingOperations . length ) {
2018-01-19 21:56:23 +03:00
for ( let op of this . pendingOperations ) { op ( ) }
2018-01-19 00:31:28 +03:00
this . pendingOperations = [ ]
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
} )
}
2018-01-18 03:30:30 +03:00
2018-03-06 22:49:41 +03:00
_clearUnscopedSettingsForSource ( source ) {
2018-03-09 08:49:18 +03:00
if ( source === this . projectFile ) {
this . projectSettings = { }
} else {
this . settings = { }
2018-03-06 22:06:22 +03:00
}
}
2018-03-02 06:09:22 +03:00
resetProjectSettings ( newSettings , projectFile ) {
2018-02-19 02:03:00 +03:00
// Sets the scope and source of all project settings to `path`.
2018-02-25 04:50:18 +03:00
newSettings = Object . assign ( { } , newSettings )
2018-03-02 06:09:22 +03:00
const oldProjectFile = this . projectFile
this . projectFile = projectFile
if ( this . projectFile != null ) {
2018-03-06 22:06:22 +03:00
this . _resetSettings ( newSettings , { source : this . projectFile } )
2018-03-02 06:09:22 +03:00
} else {
this . scopedSettingsStore . removePropertiesForSource ( oldProjectFile )
this . projectSettings = { }
}
2018-02-19 02:03:00 +03:00
}
2018-02-27 01:38:50 +03:00
clearProjectSettings ( ) {
2018-03-02 06:09:22 +03:00
this . resetProjectSettings ( { } , null )
2018-02-19 02:03:00 +03:00
}
2018-01-19 01:32:01 +03:00
getRawValue ( keyPath , options = { } ) {
2018-01-24 23:32:04 +03:00
let value
2018-01-25 05:16:49 +03:00
if ( ! options . excludeSources || ! options . excludeSources . includes ( this . mainSource ) ) {
2018-01-18 22:12:57 +03:00
value = getValueAtKeyPath ( this . settings , keyPath )
2018-03-02 06:09:22 +03:00
if ( this . projectFile != null ) {
const projectValue = getValueAtKeyPath ( this . projectSettings , keyPath )
2018-03-09 08:49:18 +03:00
value = ( projectValue === undefined ) ? value : projectValue
2018-03-02 06:09:22 +03:00
}
2018-01-18 22:12:57 +03:00
}
2018-01-19 01:32:01 +03:00
2018-01-24 23:32:04 +03:00
let defaultValue
if ( ! options . sources || options . sources . length === 0 ) {
2018-01-18 22:12:57 +03:00
defaultValue = getValueAtKeyPath ( this . defaultSettings , keyPath )
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
if ( value != null ) {
value = this . deepClone ( value )
2018-01-24 23:32:04 +03:00
if ( isPlainObject ( value ) && isPlainObject ( defaultValue ) ) {
this . deepDefaults ( value , defaultValue )
}
return value
2018-01-18 22:12:57 +03:00
} else {
2018-01-24 23:32:04 +03:00
return this . deepClone ( defaultValue )
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
}
2018-03-02 06:09:22 +03:00
setRawValue ( keyPath , value , options = { } ) {
const source = options . source ? options . source : undefined
const settingsToChange = source === this . projectFile ? 'projectSettings' : 'settings'
2018-01-18 22:12:57 +03:00
const defaultValue = getValueAtKeyPath ( this . defaultSettings , keyPath )
2018-03-02 06:09:22 +03:00
2018-01-18 22:12:57 +03:00
if ( _ . isEqual ( defaultValue , value ) ) {
if ( keyPath != null ) {
2018-03-02 06:09:22 +03:00
deleteValueAtKeyPath ( this [ settingsToChange ] , keyPath )
2018-01-18 03:30:30 +03:00
} else {
2018-03-02 06:09:22 +03:00
this [ settingsToChange ] = null
2018-01-18 22:12:57 +03:00
}
} else {
if ( keyPath != null ) {
2018-03-02 06:09:22 +03:00
setValueAtKeyPath ( this [ settingsToChange ] , keyPath , value )
2018-01-18 22:12:57 +03:00
} else {
2018-03-02 06:09:22 +03:00
this [ settingsToChange ] = value
2018-01-18 03:30:30 +03:00
}
}
2018-01-18 22:12:57 +03:00
return this . emitChangeEvent ( )
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
observeKeyPath ( keyPath , options , callback ) {
callback ( this . get ( keyPath ) )
return this . onDidChangeKeyPath ( keyPath , event => callback ( event . newValue ) )
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
onDidChangeKeyPath ( keyPath , callback ) {
let oldValue = this . get ( keyPath )
return this . emitter . on ( 'did-change' , ( ) => {
const newValue = this . get ( keyPath )
if ( ! _ . isEqual ( oldValue , newValue ) ) {
const event = { oldValue , newValue }
oldValue = newValue
return callback ( event )
}
} )
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
isSubKeyPath ( keyPath , subKeyPath ) {
if ( ( keyPath == null ) || ( subKeyPath == null ) ) { return false }
const pathSubTokens = splitKeyPath ( subKeyPath )
const pathTokens = splitKeyPath ( keyPath ) . slice ( 0 , pathSubTokens . length )
return _ . isEqual ( pathTokens , pathSubTokens )
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
setRawDefault ( keyPath , value ) {
setValueAtKeyPath ( this . defaultSettings , keyPath , value )
return this . emitChangeEvent ( )
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
setDefaults ( keyPath , defaults ) {
if ( ( defaults != null ) && isPlainObject ( defaults ) ) {
const keys = splitKeyPath ( keyPath )
this . transact ( ( ) => {
2018-01-19 21:56:23 +03:00
const result = [ ]
for ( let key in defaults ) {
const childValue = defaults [ key ]
if ( ! defaults . hasOwnProperty ( key ) ) { continue }
result . push ( this . setDefaults ( keys . concat ( [ key ] ) . join ( '.' ) , childValue ) )
}
return result
2018-01-18 22:12:57 +03:00
} )
} else {
try {
defaults = this . makeValueConformToSchema ( keyPath , defaults )
this . setRawDefault ( keyPath , defaults )
} catch ( e ) {
console . warn ( ` ' ${ keyPath } ' could not set the default. Attempted default: ${ JSON . stringify ( defaults ) } ; Schema: ${ JSON . stringify ( this . getSchema ( keyPath ) ) } ` )
2018-01-18 03:30:30 +03:00
}
}
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
deepClone ( object ) {
if ( object instanceof Color ) {
return object . clone ( )
2018-03-03 19:18:35 +03:00
} else if ( Array . isArray ( object ) ) {
2018-01-18 22:12:57 +03:00
return object . map ( value => this . deepClone ( value ) )
} else if ( isPlainObject ( object ) ) {
return _ . mapObject ( object , ( key , value ) => [ key , this . deepClone ( value ) ] )
} else {
return object
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
deepDefaults ( target ) {
let result = target
let i = 0
while ( ++ i < arguments . length ) {
const object = arguments [ i ]
if ( isPlainObject ( result ) && isPlainObject ( object ) ) {
2018-01-19 21:56:23 +03:00
for ( let key of Object . keys ( object ) ) {
2018-01-18 22:12:57 +03:00
result [ key ] = this . deepDefaults ( result [ key ] , object [ key ] )
}
} else {
if ( ( result == null ) ) {
result = this . deepClone ( object )
2018-01-18 03:30:30 +03:00
}
}
}
2018-01-18 22:12:57 +03:00
return result
}
2018-01-18 03:30:30 +03:00
2018-01-19 03:46:00 +03:00
// `schema` will look something like this
//
// ```coffee
// type: 'string'
// default: 'ok'
// scopes:
// '.source.js':
// default: 'omg'
// ```
2018-01-18 22:12:57 +03:00
setScopedDefaultsFromSchema ( keyPath , schema ) {
if ( ( schema . scopes != null ) && isPlainObject ( schema . scopes ) ) {
const scopedDefaults = { }
for ( let scope in schema . scopes ) {
const scopeSchema = schema . scopes [ scope ]
if ( ! scopeSchema . hasOwnProperty ( 'default' ) ) { continue }
scopedDefaults [ scope ] = { }
setValueAtKeyPath ( scopedDefaults [ scope ] , keyPath , scopeSchema . default )
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
this . scopedSettingsStore . addProperties ( 'schema-default' , scopedDefaults )
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
if ( ( schema . type === 'object' ) && ( schema . properties != null ) && isPlainObject ( schema . properties ) ) {
const keys = splitKeyPath ( keyPath )
for ( let key in schema . properties ) {
const childValue = schema . properties [ key ]
if ( ! schema . properties . hasOwnProperty ( key ) ) { continue }
this . setScopedDefaultsFromSchema ( keys . concat ( [ key ] ) . join ( '.' ) , childValue )
2018-01-18 03:30:30 +03:00
}
}
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
extractDefaultsFromSchema ( schema ) {
if ( schema . default != null ) {
return schema . default
} else if ( ( schema . type === 'object' ) && ( schema . properties != null ) && isPlainObject ( schema . properties ) ) {
const defaults = { }
const properties = schema . properties || { }
for ( let key in properties ) { const value = properties [ key ] ; defaults [ key ] = this . extractDefaultsFromSchema ( value ) }
return defaults
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
makeValueConformToSchema ( keyPath , value , options ) {
if ( options != null ? options . suppressException : undefined ) {
try {
return this . makeValueConformToSchema ( keyPath , value )
} catch ( e ) {
return undefined
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
} else {
let schema
if ( ( schema = this . getSchema ( keyPath ) ) == null ) {
if ( schema === false ) { throw new Error ( ` Illegal key path ${ keyPath } ` ) }
}
return this . constructor . executeSchemaEnforcers ( keyPath , value , schema )
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-19 03:46:00 +03:00
// When the schema is changed / added, there may be values set in the config
// that do not conform to the schema. This will reset make them conform.
2018-01-18 22:12:57 +03:00
resetSettingsForSchemaChange ( source ) {
2018-01-25 05:16:49 +03:00
if ( source == null ) { source = this . mainSource }
2018-01-18 22:12:57 +03:00
return this . transact ( ( ) => {
this . settings = this . makeValueConformToSchema ( null , this . settings , { suppressException : true } )
const selectorsAndSettings = this . scopedSettingsStore . propertiesForSource ( source )
this . scopedSettingsStore . removePropertiesForSource ( source )
for ( let scopeSelector in selectorsAndSettings ) {
let settings = selectorsAndSettings [ scopeSelector ]
settings = this . makeValueConformToSchema ( null , settings , { suppressException : true } )
this . setRawScopedValue ( null , settings , source , scopeSelector )
}
} )
}
2018-01-18 03:30:30 +03:00
2018-01-19 03:46:00 +03:00
/ *
Section : Private Scoped Settings
* /
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
priorityForSource ( source ) {
2018-03-02 06:09:22 +03:00
switch ( source ) {
case this . mainSource :
return 1000
case this . projectFile :
return 2000
default :
return 0
}
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
emitChangeEvent ( ) {
2018-01-19 21:56:23 +03:00
if ( this . transactDepth <= 0 ) { return this . emitter . emit ( 'did-change' ) }
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-03-06 22:06:22 +03:00
resetScopedSettings ( newScopedSettings , options = { } ) {
2018-02-19 02:03:00 +03:00
const source = options . source == null ? this . mainSource : options . source
2018-01-18 22:12:57 +03:00
const priority = this . priorityForSource ( source )
this . scopedSettingsStore . removePropertiesForSource ( source )
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
for ( let scopeSelector in newScopedSettings ) {
let settings = newScopedSettings [ scopeSelector ]
settings = this . makeValueConformToSchema ( null , settings , { suppressException : true } )
const validatedSettings = { }
validatedSettings [ scopeSelector ] = withoutEmptyObjects ( settings )
if ( validatedSettings [ scopeSelector ] != null ) { this . scopedSettingsStore . addProperties ( source , validatedSettings , { priority } ) }
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
return this . emitChangeEvent ( )
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
setRawScopedValue ( keyPath , value , source , selector , options ) {
if ( keyPath != null ) {
const newValue = { }
setValueAtKeyPath ( newValue , keyPath , value )
value = newValue
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
const settingsBySelector = { }
settingsBySelector [ selector ] = value
this . scopedSettingsStore . addProperties ( source , settingsBySelector , { priority : this . priorityForSource ( source ) } )
return this . emitChangeEvent ( )
}
getRawScopedValue ( scopeDescriptor , keyPath , options ) {
scopeDescriptor = ScopeDescriptor . fromObject ( scopeDescriptor )
const result = this . scopedSettingsStore . getPropertyValue (
2018-01-20 04:11:16 +03:00
scopeDescriptor . getScopeChain ( ) ,
keyPath ,
options
)
2018-01-18 03:30:30 +03:00
2018-02-20 21:52:16 +03:00
const legacyScopeDescriptor = this . getLegacyScopeDescriptorForNewScopeDescriptor ( scopeDescriptor )
2018-01-18 22:12:57 +03:00
if ( result != null ) {
return result
2018-01-19 00:31:28 +03:00
} else if ( legacyScopeDescriptor ) {
2018-01-18 22:12:57 +03:00
return this . scopedSettingsStore . getPropertyValue (
2018-01-20 04:11:16 +03:00
legacyScopeDescriptor . getScopeChain ( ) ,
keyPath ,
options
)
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
observeScopedKeyPath ( scope , keyPath , callback ) {
callback ( this . get ( keyPath , { scope } ) )
return this . onDidChangeScopedKeyPath ( scope , keyPath , event => callback ( event . newValue ) )
}
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
onDidChangeScopedKeyPath ( scope , keyPath , callback ) {
let oldValue = this . get ( keyPath , { scope } )
return this . emitter . on ( 'did-change' , ( ) => {
const newValue = this . get ( keyPath , { scope } )
if ( ! _ . isEqual ( oldValue , newValue ) ) {
const event = { oldValue , newValue }
oldValue = newValue
2018-01-19 22:01:32 +03:00
callback ( event )
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
} )
}
2018-01-19 22:01:32 +03:00
} ;
2018-01-18 03:30:30 +03:00
// 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.
//
// 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 . addSchemaEnforcers ( {
'any' : {
2018-01-18 22:12:57 +03:00
coerce ( keyPath , value , schema ) {
return value
2018-01-18 03:30:30 +03:00
}
} ,
'integer' : {
2018-01-18 22:12:57 +03:00
coerce ( keyPath , value , schema ) {
value = parseInt ( value )
if ( isNaN ( value ) || ! isFinite ( value ) ) { throw new Error ( ` Validation failed at ${ keyPath } , ${ JSON . stringify ( value ) } cannot be coerced into an int ` ) }
return value
2018-01-18 03:30:30 +03:00
}
} ,
'number' : {
2018-01-18 22:12:57 +03:00
coerce ( keyPath , value , schema ) {
value = parseFloat ( value )
if ( isNaN ( value ) || ! isFinite ( value ) ) { throw new Error ( ` Validation failed at ${ keyPath } , ${ JSON . stringify ( value ) } cannot be coerced into a number ` ) }
return value
2018-01-18 03:30:30 +03:00
}
} ,
'boolean' : {
2018-01-18 22:12:57 +03:00
coerce ( keyPath , value , schema ) {
2018-01-18 03:30:30 +03:00
switch ( typeof value ) {
case 'string' :
if ( value . toLowerCase ( ) === 'true' ) {
2018-01-18 22:12:57 +03:00
return true
2018-01-18 03:30:30 +03:00
} else if ( value . toLowerCase ( ) === 'false' ) {
2018-01-18 22:12:57 +03:00
return false
2018-01-18 03:30:30 +03:00
} else {
2018-01-18 22:12:57 +03:00
throw new Error ( ` Validation failed at ${ keyPath } , ${ JSON . stringify ( value ) } must be a boolean or the string 'true' or 'false' ` )
2018-01-18 03:30:30 +03:00
}
case 'boolean' :
2018-01-18 22:12:57 +03:00
return value
2018-01-18 03:30:30 +03:00
default :
2018-01-18 22:12:57 +03:00
throw new Error ( ` Validation failed at ${ keyPath } , ${ JSON . stringify ( value ) } must be a boolean or the string 'true' or 'false' ` )
2018-01-18 03:30:30 +03:00
}
}
} ,
'string' : {
2018-01-18 22:12:57 +03:00
validate ( keyPath , value , schema ) {
2018-01-18 03:30:30 +03:00
if ( typeof value !== 'string' ) {
2018-01-18 22:12:57 +03:00
throw new Error ( ` Validation failed at ${ keyPath } , ${ JSON . stringify ( value ) } must be a string ` )
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
return value
2018-01-18 03:30:30 +03:00
} ,
2018-01-18 22:12:57 +03:00
validateMaximumLength ( keyPath , value , schema ) {
2018-01-18 03:30:30 +03:00
if ( ( typeof schema . maximumLength === 'number' ) && ( value . length > schema . maximumLength ) ) {
2018-01-18 22:12:57 +03:00
return value . slice ( 0 , schema . maximumLength )
2018-01-18 03:30:30 +03:00
} else {
2018-01-18 22:12:57 +03:00
return value
2018-01-18 03:30:30 +03:00
}
}
} ,
'null' : {
// null sort of isnt supported. It will just unset in this case
2018-01-18 22:12:57 +03:00
coerce ( keyPath , value , schema ) {
if ( ! [ undefined , null ] . includes ( value ) ) { throw new Error ( ` Validation failed at ${ keyPath } , ${ JSON . stringify ( value ) } must be null ` ) }
return value
2018-01-18 03:30:30 +03:00
}
} ,
'object' : {
2018-01-18 22:12:57 +03:00
coerce ( keyPath , value , schema ) {
if ( ! isPlainObject ( value ) ) { throw new Error ( ` Validation failed at ${ keyPath } , ${ JSON . stringify ( value ) } must be an object ` ) }
if ( schema . properties == null ) { return value }
2018-01-18 03:30:30 +03:00
2018-01-18 22:12:57 +03:00
let defaultChildSchema = null
let allowsAdditionalProperties = true
2018-01-18 03:30:30 +03:00
if ( isPlainObject ( schema . additionalProperties ) ) {
2018-01-18 22:12:57 +03:00
defaultChildSchema = schema . additionalProperties
2018-01-18 03:30:30 +03:00
}
if ( schema . additionalProperties === false ) {
2018-01-18 22:12:57 +03:00
allowsAdditionalProperties = false
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
const newValue = { }
2018-01-18 03:30:30 +03:00
for ( let prop in value ) {
2018-01-18 22:12:57 +03:00
const propValue = value [ prop ]
const childSchema = schema . properties [ prop ] != null ? schema . properties [ prop ] : defaultChildSchema
2018-01-18 03:30:30 +03:00
if ( childSchema != null ) {
try {
2018-01-18 22:12:57 +03:00
newValue [ prop ] = this . executeSchemaEnforcers ( pushKeyPath ( keyPath , prop ) , propValue , childSchema )
2018-01-18 03:30:30 +03:00
} catch ( error ) {
2018-01-18 22:12:57 +03:00
console . warn ( ` Error setting item in object: ${ error . message } ` )
2018-01-18 03:30:30 +03:00
}
} else if ( allowsAdditionalProperties ) {
// Just pass through un-schema'd values
2018-01-18 22:12:57 +03:00
newValue [ prop ] = propValue
2018-01-18 03:30:30 +03:00
} else {
2018-01-18 22:12:57 +03:00
console . warn ( ` Illegal object key: ${ keyPath } . ${ prop } ` )
2018-01-18 03:30:30 +03:00
}
}
2018-01-18 22:12:57 +03:00
return newValue
2018-01-18 03:30:30 +03:00
}
} ,
'array' : {
2018-01-18 22:12:57 +03:00
coerce ( keyPath , value , schema ) {
if ( ! Array . isArray ( value ) ) { throw new Error ( ` Validation failed at ${ keyPath } , ${ JSON . stringify ( value ) } must be an array ` ) }
const itemSchema = schema . items
2018-01-18 03:30:30 +03:00
if ( itemSchema != null ) {
2018-01-18 22:12:57 +03:00
const newValue = [ ]
2018-01-19 22:01:32 +03:00
for ( let item of value ) {
2018-01-18 03:30:30 +03:00
try {
2018-01-18 22:12:57 +03:00
newValue . push ( this . executeSchemaEnforcers ( keyPath , item , itemSchema ) )
2018-01-18 03:30:30 +03:00
} catch ( error ) {
2018-01-18 22:12:57 +03:00
console . warn ( ` Error setting item in array: ${ error . message } ` )
2018-01-18 03:30:30 +03:00
}
}
2018-01-18 22:12:57 +03:00
return newValue
2018-01-18 03:30:30 +03:00
} else {
2018-01-18 22:12:57 +03:00
return value
2018-01-18 03:30:30 +03:00
}
}
} ,
'color' : {
2018-01-18 22:12:57 +03:00
coerce ( keyPath , value , schema ) {
const color = Color . parse ( value )
2018-01-18 03:30:30 +03:00
if ( color == null ) {
2018-01-18 22:12:57 +03:00
throw new Error ( ` Validation failed at ${ keyPath } , ${ JSON . stringify ( value ) } cannot be coerced into a color ` )
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
return color
2018-01-18 03:30:30 +03:00
}
} ,
'*' : {
2018-01-18 22:12:57 +03:00
coerceMinimumAndMaximum ( keyPath , value , schema ) {
if ( typeof value !== 'number' ) { return value }
2018-01-18 03:30:30 +03:00
if ( ( schema . minimum != null ) && ( typeof schema . minimum === 'number' ) ) {
2018-01-18 22:12:57 +03:00
value = Math . max ( value , schema . minimum )
2018-01-18 03:30:30 +03:00
}
if ( ( schema . maximum != null ) && ( typeof schema . maximum === 'number' ) ) {
2018-01-18 22:12:57 +03:00
value = Math . min ( value , schema . maximum )
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
return value
2018-01-18 03:30:30 +03:00
} ,
2018-01-18 22:12:57 +03:00
validateEnum ( keyPath , value , schema ) {
let possibleValues = schema . enum
2018-01-18 03:30:30 +03:00
if ( Array . isArray ( possibleValues ) ) {
2018-01-19 01:52:47 +03:00
possibleValues = possibleValues . map ( value => {
2018-01-18 22:12:57 +03:00
if ( value . hasOwnProperty ( 'value' ) ) { return value . value } else { return value }
} )
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
if ( ( possibleValues == null ) || ! Array . isArray ( possibleValues ) || ! possibleValues . length ) { return value }
2018-01-18 03:30:30 +03:00
2018-01-19 22:01:32 +03:00
for ( let possibleValue of possibleValues ) {
2018-01-18 03:30:30 +03:00
// Using `isEqual` for possibility of placing enums on array and object schemas
2018-01-18 22:12:57 +03:00
if ( _ . isEqual ( possibleValue , value ) ) { return value }
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
throw new Error ( ` Validation failed at ${ keyPath } , ${ JSON . stringify ( value ) } is not one of ${ JSON . stringify ( possibleValues ) } ` )
2018-01-18 03:30:30 +03:00
}
}
2018-01-18 22:12:57 +03:00
} )
2018-01-18 03:30:30 +03:00
2018-03-03 19:18:35 +03:00
let isPlainObject = value => _ . isObject ( value ) && ! Array . isArray ( value ) && ! _ . isFunction ( value ) && ! _ . isString ( value ) && ! ( value instanceof Color )
2018-01-18 03:30:30 +03:00
2018-01-19 01:52:47 +03:00
let sortObject = value => {
2018-01-18 22:12:57 +03:00
if ( ! isPlainObject ( value ) ) { return value }
const result = { }
2018-01-19 22:01:32 +03:00
for ( let key of Object . keys ( value ) . sort ( ) ) {
2018-01-18 22:12:57 +03:00
result [ key ] = sortObject ( value [ key ] )
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
return result
}
2018-01-18 03:30:30 +03:00
2018-01-19 01:52:47 +03:00
const withoutEmptyObjects = ( object ) => {
2018-01-18 22:12:57 +03:00
let resultObject
2018-01-18 03:30:30 +03:00
if ( isPlainObject ( object ) ) {
for ( let key in object ) {
2018-01-18 22:12:57 +03:00
const value = object [ key ]
const newValue = withoutEmptyObjects ( value )
2018-01-18 03:30:30 +03:00
if ( newValue != null ) {
2018-01-18 22:12:57 +03:00
if ( resultObject == null ) { resultObject = { } }
resultObject [ key ] = newValue
2018-01-18 03:30:30 +03:00
}
}
} else {
2018-01-18 22:12:57 +03:00
resultObject = object
2018-01-18 03:30:30 +03:00
}
2018-01-18 22:12:57 +03:00
return resultObject
}
2018-01-19 03:46:00 +03:00
module . exports = Config